@apliteni/apliteni-ui 0.90.0 → 0.92.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/package.json +1 -1
- package/react/dist/index.js +177 -125
- package/src/components/dropdown.js +252 -2
- package/src/components/filter-bar.js +2 -2
- package/src/index.d.ts +18 -0
- package/src/index.js +1 -1
- package/src/logic/filter-bar.js +20 -0
- package/src/motion.d.ts +2 -0
- package/src/motion.js +27 -0
- package/src/styles/filter-bar.css +55 -10
- package/src/styles/topbar.css +1 -1
package/package.json
CHANGED
package/react/dist/index.js
CHANGED
|
@@ -433,6 +433,7 @@ import {
|
|
|
433
433
|
useRef as useRef3,
|
|
434
434
|
useState as useState2
|
|
435
435
|
} from "react";
|
|
436
|
+
import { transitionMs } from "@apliteni/apliteni-ui";
|
|
436
437
|
var FOCUSABLE = 'a[href], button:not([disabled]), input:not([disabled]), select:not([disabled]), textarea:not([disabled]), [tabindex]:not([tabindex="-1"]), details > summary:first-of-type';
|
|
437
438
|
function tabbable(el) {
|
|
438
439
|
const tabindex = el.getAttribute("tabindex");
|
|
@@ -453,17 +454,6 @@ var dismissOnScrim = (onClose) => (e) => {
|
|
|
453
454
|
onClose();
|
|
454
455
|
};
|
|
455
456
|
var useIsoLayoutEffect = typeof document === "undefined" ? useEffect2 : useLayoutEffect3;
|
|
456
|
-
function transitionMs(el) {
|
|
457
|
-
const cs = getComputedStyle(el);
|
|
458
|
-
const times = (list) => list.split(",").map((t) => {
|
|
459
|
-
const n = Number.parseFloat(t);
|
|
460
|
-
if (!Number.isFinite(n)) return 0;
|
|
461
|
-
return t.trim().endsWith("ms") ? n : n * 1e3;
|
|
462
|
-
});
|
|
463
|
-
const durations = times(cs.transitionDuration);
|
|
464
|
-
const delays = times(cs.transitionDelay);
|
|
465
|
-
return Math.max(0, ...durations.map((d, i) => d + delays[i % delays.length]));
|
|
466
|
-
}
|
|
467
457
|
var DialogScope = createContext(null);
|
|
468
458
|
var stack = [];
|
|
469
459
|
var leaving = /* @__PURE__ */ new Set();
|
|
@@ -945,7 +935,7 @@ function rowId(uid, groups, item) {
|
|
|
945
935
|
}
|
|
946
936
|
|
|
947
937
|
// src/DataTable.tsx
|
|
948
|
-
import {
|
|
938
|
+
import { useMemo as useMemo2, useState as useState5 } from "react";
|
|
949
939
|
import { DEFAULT_PAGE_SIZE as DEFAULT_PAGE_SIZE2 } from "@apliteni/apliteni-ui";
|
|
950
940
|
|
|
951
941
|
// src/Pagination.tsx
|
|
@@ -1209,31 +1199,6 @@ function DataTable({
|
|
|
1209
1199
|
hasMore = false
|
|
1210
1200
|
}) {
|
|
1211
1201
|
const scrollable = stickyHeader || pinnedIdentity;
|
|
1212
|
-
const scrollId = useId7();
|
|
1213
|
-
const scrollRegion = useRef9(null);
|
|
1214
|
-
const [columnScroll, setColumnScroll] = useState5({ overflow: false, start: true, end: false });
|
|
1215
|
-
const measureColumns = () => {
|
|
1216
|
-
const region = scrollRegion.current;
|
|
1217
|
-
if (region) setColumnScroll({
|
|
1218
|
-
overflow: region.scrollWidth > region.clientWidth + 1,
|
|
1219
|
-
start: region.scrollLeft <= 1,
|
|
1220
|
-
end: region.scrollLeft + region.clientWidth >= region.scrollWidth - 1
|
|
1221
|
-
});
|
|
1222
|
-
};
|
|
1223
|
-
useEffect5(() => {
|
|
1224
|
-
if (!scrollable) return;
|
|
1225
|
-
const region = scrollRegion.current;
|
|
1226
|
-
if (!region) return;
|
|
1227
|
-
const observer = typeof ResizeObserver === "undefined" ? null : new ResizeObserver(measureColumns);
|
|
1228
|
-
observer?.observe(region);
|
|
1229
|
-
if (region.firstElementChild) observer?.observe(region.firstElementChild);
|
|
1230
|
-
measureColumns();
|
|
1231
|
-
return () => observer?.disconnect();
|
|
1232
|
-
}, [scrollable, columns, rows]);
|
|
1233
|
-
const scrollColumns = (direction) => {
|
|
1234
|
-
const region = scrollRegion.current;
|
|
1235
|
-
if (region) region.scrollBy({ left: direction * region.clientWidth / 2, behavior: "instant" });
|
|
1236
|
-
};
|
|
1237
1202
|
const [localSort, setLocalSort] = useState5(
|
|
1238
1203
|
{ key: columns.find((c) => c.sortable)?.key, dir: -1 }
|
|
1239
1204
|
);
|
|
@@ -1286,20 +1251,13 @@ function DataTable({
|
|
|
1286
1251
|
);
|
|
1287
1252
|
const pageAllOn = selectable && slice.length > 0 && slice.every((r) => selected.has(r.name));
|
|
1288
1253
|
return /* @__PURE__ */ jsxs13(Fragment3, { children: [
|
|
1289
|
-
scrollable && columnScroll.overflow && /* @__PURE__ */ jsxs13("div", { className: "ui-card__row", role: "group", "aria-label": `${scrollLabel} columns`, children: [
|
|
1290
|
-
/* @__PURE__ */ jsx17(Button, { size: "sm", icon: "arrowLeft", "aria-controls": scrollId, disabled: columnScroll.start, onClick: () => scrollColumns(-1), children: "Previous columns" }),
|
|
1291
|
-
/* @__PURE__ */ jsx17(Button, { size: "sm", iconRight: "arrowRight", "aria-controls": scrollId, disabled: columnScroll.end, onClick: () => scrollColumns(1), children: "More columns" })
|
|
1292
|
-
] }),
|
|
1293
1254
|
/* @__PURE__ */ jsx17(
|
|
1294
1255
|
"div",
|
|
1295
1256
|
{
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
role: stickyHeader || pinnedIdentity ? "region" : void 0,
|
|
1301
|
-
"aria-label": stickyHeader || pinnedIdentity ? scrollLabel : void 0,
|
|
1302
|
-
tabIndex: stickyHeader || pinnedIdentity ? 0 : void 0,
|
|
1257
|
+
className: scrollable ? "ui-table-scroll" : void 0,
|
|
1258
|
+
role: scrollable ? "region" : void 0,
|
|
1259
|
+
"aria-label": scrollable ? scrollLabel : void 0,
|
|
1260
|
+
tabIndex: scrollable ? 0 : void 0,
|
|
1303
1261
|
children: /* @__PURE__ */ jsxs13(
|
|
1304
1262
|
"table",
|
|
1305
1263
|
{
|
|
@@ -1452,9 +1410,9 @@ function Denied({
|
|
|
1452
1410
|
// src/DatePicker.tsx
|
|
1453
1411
|
import {
|
|
1454
1412
|
useCallback as useCallback2,
|
|
1455
|
-
useEffect as
|
|
1413
|
+
useEffect as useEffect5,
|
|
1456
1414
|
useMemo as useMemo3,
|
|
1457
|
-
useRef as
|
|
1415
|
+
useRef as useRef9,
|
|
1458
1416
|
useState as useState6
|
|
1459
1417
|
} from "react";
|
|
1460
1418
|
import { jsx as jsx19, jsxs as jsxs15 } from "react/jsx-runtime";
|
|
@@ -1467,7 +1425,7 @@ var DAY_RE = /^(\d{4})-(\d{2})-(\d{2})$/;
|
|
|
1467
1425
|
var PHONE_MAX = 560;
|
|
1468
1426
|
function usePhone() {
|
|
1469
1427
|
const [phone, setPhone] = useState6(false);
|
|
1470
|
-
|
|
1428
|
+
useEffect5(() => {
|
|
1471
1429
|
const query = typeof window === "undefined" ? null : window.matchMedia?.(`(max-width: ${PHONE_MAX}px)`);
|
|
1472
1430
|
if (!query) return;
|
|
1473
1431
|
const read = () => setPhone(query.matches);
|
|
@@ -1562,9 +1520,9 @@ function DatePicker({
|
|
|
1562
1520
|
const picksRange = mode === "range" || mode === "day-range";
|
|
1563
1521
|
const phone = usePhone();
|
|
1564
1522
|
const asSheet = sheet ?? phone;
|
|
1565
|
-
const root =
|
|
1566
|
-
const trigger =
|
|
1567
|
-
const grid =
|
|
1523
|
+
const root = useRef9(null);
|
|
1524
|
+
const trigger = useRef9(null);
|
|
1525
|
+
const grid = useRef9(null);
|
|
1568
1526
|
const [selfOpen, setSelfOpen] = useState6(defaultOpen);
|
|
1569
1527
|
const open = openProp ?? selfOpen;
|
|
1570
1528
|
const [selfValue, setSelfValue] = useState6(defaultValue);
|
|
@@ -1601,19 +1559,19 @@ function DatePicker({
|
|
|
1601
1559
|
}, [today, grain]);
|
|
1602
1560
|
const anchor = toIndex(picked, grain) ?? toIndex(picksRange ? span.start ?? span.end : null, grain) ?? todayIndex;
|
|
1603
1561
|
const [cursor, setCursor] = useState6(() => clamp(anchor, lo, hi));
|
|
1604
|
-
const land =
|
|
1562
|
+
const land = useRef9(false);
|
|
1605
1563
|
const setOpen = useCallback2((next) => {
|
|
1606
1564
|
if (openProp === void 0) setSelfOpen(next);
|
|
1607
1565
|
onOpenChange?.(next);
|
|
1608
1566
|
}, [openProp, onOpenChange]);
|
|
1609
|
-
const close =
|
|
1567
|
+
const close = useRef9(setOpen);
|
|
1610
1568
|
useIsoLayoutEffect(() => {
|
|
1611
1569
|
close.current = setOpen;
|
|
1612
1570
|
});
|
|
1613
|
-
|
|
1571
|
+
useEffect5(() => {
|
|
1614
1572
|
if (open) setCursor(clamp(anchor, lo, hi));
|
|
1615
1573
|
}, [open]);
|
|
1616
|
-
|
|
1574
|
+
useEffect5(() => {
|
|
1617
1575
|
if (!open || asSheet) return;
|
|
1618
1576
|
const doc = root.current?.ownerDocument ?? document;
|
|
1619
1577
|
const onClick = (e) => {
|
|
@@ -1923,13 +1881,24 @@ function DatePicker({
|
|
|
1923
1881
|
import {
|
|
1924
1882
|
Fragment as Fragment4,
|
|
1925
1883
|
useCallback as useCallback3,
|
|
1926
|
-
useEffect as
|
|
1927
|
-
useId as
|
|
1928
|
-
useRef as
|
|
1884
|
+
useEffect as useEffect6,
|
|
1885
|
+
useId as useId7,
|
|
1886
|
+
useRef as useRef10,
|
|
1929
1887
|
useState as useState7
|
|
1930
1888
|
} from "react";
|
|
1931
|
-
import {
|
|
1889
|
+
import {
|
|
1890
|
+
icon as icon3,
|
|
1891
|
+
dropdownMatch,
|
|
1892
|
+
dropdownFiltering,
|
|
1893
|
+
filterPanelFit,
|
|
1894
|
+
transitionMs as transitionMs2
|
|
1895
|
+
} from "@apliteni/apliteni-ui";
|
|
1932
1896
|
import { Fragment as Fragment5, jsx as jsx20, jsxs as jsxs16 } from "react/jsx-runtime";
|
|
1897
|
+
var EXIT_SLACK_MS2 = 50;
|
|
1898
|
+
var inPanel = (node) => {
|
|
1899
|
+
const el = node?.nodeType === 1 ? node : node?.parentElement ?? null;
|
|
1900
|
+
return !!el?.closest("[data-dropdown-panel]");
|
|
1901
|
+
};
|
|
1933
1902
|
var cx6 = (...a) => a.filter(Boolean).join(" ");
|
|
1934
1903
|
var isRow = (it) => Boolean(it) && it !== "---" && !it.separator;
|
|
1935
1904
|
var Glyph2 = ({ name, className, hidden }) => /* @__PURE__ */ jsx20(
|
|
@@ -1986,22 +1955,100 @@ function Dropdown({
|
|
|
1986
1955
|
const [pick, setPick] = useState7(null);
|
|
1987
1956
|
const [query, setQuery] = useState7(() => search && search !== true ? search.query || "" : "");
|
|
1988
1957
|
const [activeKey, setActiveKey] = useState7(null);
|
|
1989
|
-
const root =
|
|
1990
|
-
const panel =
|
|
1991
|
-
const trigger =
|
|
1992
|
-
const field =
|
|
1993
|
-
const landOn =
|
|
1994
|
-
const at =
|
|
1995
|
-
const auto =
|
|
1958
|
+
const root = useRef10(null);
|
|
1959
|
+
const panel = useRef10(null);
|
|
1960
|
+
const trigger = useRef10(null);
|
|
1961
|
+
const field = useRef10(null);
|
|
1962
|
+
const landOn = useRef10(null);
|
|
1963
|
+
const at = useRef10("");
|
|
1964
|
+
const auto = useId7().replace(/:/g, "");
|
|
1996
1965
|
const uid = id ?? auto;
|
|
1997
1966
|
const setOpen = useCallback3((next) => {
|
|
1998
1967
|
if (openProp === void 0) setSelfOpen(next);
|
|
1999
1968
|
onOpenChange?.(next);
|
|
2000
1969
|
}, [openProp, onOpenChange]);
|
|
2001
|
-
const close =
|
|
1970
|
+
const close = useRef10(setOpen);
|
|
2002
1971
|
useIsoLayoutEffect(() => {
|
|
2003
1972
|
close.current = setOpen;
|
|
2004
1973
|
});
|
|
1974
|
+
useIsoLayoutEffect(() => {
|
|
1975
|
+
const el = panel.current;
|
|
1976
|
+
if (!el || !open) return;
|
|
1977
|
+
const fit = () => {
|
|
1978
|
+
const got = filterPanelFit(root.current);
|
|
1979
|
+
if (!got) return;
|
|
1980
|
+
let moved = false;
|
|
1981
|
+
for (const prop of ["room", "shift", "floor"]) {
|
|
1982
|
+
const name2 = `--ui-filter-panel-${prop}`;
|
|
1983
|
+
const next = `${got[prop]}px`;
|
|
1984
|
+
if (el.style.getPropertyValue(name2) === next) continue;
|
|
1985
|
+
el.style.setProperty(name2, next);
|
|
1986
|
+
moved = true;
|
|
1987
|
+
}
|
|
1988
|
+
if (!moved) return;
|
|
1989
|
+
if (!el.style.minWidth) return;
|
|
1990
|
+
el.style.minWidth = "";
|
|
1991
|
+
if (el.offsetWidth) el.style.minWidth = `${el.offsetWidth}px`;
|
|
1992
|
+
};
|
|
1993
|
+
fit();
|
|
1994
|
+
const view = root.current?.ownerDocument?.defaultView;
|
|
1995
|
+
const row2 = root.current?.closest(".ui-filter-bar__chip")?.closest(".ui-filter-bar");
|
|
1996
|
+
if (!row2 || !view) return;
|
|
1997
|
+
const stop = [];
|
|
1998
|
+
let ro = null;
|
|
1999
|
+
if (typeof view.ResizeObserver === "function") {
|
|
2000
|
+
ro = new view.ResizeObserver(() => fit());
|
|
2001
|
+
ro.observe(row2);
|
|
2002
|
+
for (const kid of Array.from(row2.children)) ro.observe(kid);
|
|
2003
|
+
stop.push(() => ro?.disconnect());
|
|
2004
|
+
} else {
|
|
2005
|
+
view.addEventListener("resize", fit);
|
|
2006
|
+
stop.push(() => view.removeEventListener("resize", fit));
|
|
2007
|
+
}
|
|
2008
|
+
if (typeof view.MutationObserver === "function") {
|
|
2009
|
+
const mo = new view.MutationObserver((records) => {
|
|
2010
|
+
if (records.every((m) => inPanel(m.target))) return;
|
|
2011
|
+
if (ro && records.some((m) => m.type === "childList")) {
|
|
2012
|
+
for (const kid of Array.from(row2.children)) ro.observe(kid);
|
|
2013
|
+
}
|
|
2014
|
+
fit();
|
|
2015
|
+
});
|
|
2016
|
+
mo.observe(row2, { subtree: true, childList: true, attributes: true, characterData: true });
|
|
2017
|
+
stop.push(() => mo.disconnect());
|
|
2018
|
+
}
|
|
2019
|
+
return () => {
|
|
2020
|
+
stop.forEach((off) => off());
|
|
2021
|
+
};
|
|
2022
|
+
}, [open]);
|
|
2023
|
+
const opened = useRef10(false);
|
|
2024
|
+
useIsoLayoutEffect(() => {
|
|
2025
|
+
const el = panel.current;
|
|
2026
|
+
if (open) {
|
|
2027
|
+
opened.current = true;
|
|
2028
|
+
return;
|
|
2029
|
+
}
|
|
2030
|
+
if (!opened.current || !el || !root.current?.closest(".ui-filter-bar__chip")) return;
|
|
2031
|
+
el.classList.add("is-closing");
|
|
2032
|
+
let done = false;
|
|
2033
|
+
const finish = () => {
|
|
2034
|
+
if (done) return;
|
|
2035
|
+
done = true;
|
|
2036
|
+
el.classList.remove("is-closing");
|
|
2037
|
+
el.style.minWidth = "";
|
|
2038
|
+
for (const prop of ["room", "shift", "floor"]) el.style.removeProperty(`--ui-filter-panel-${prop}`);
|
|
2039
|
+
};
|
|
2040
|
+
const onEnd = (e) => {
|
|
2041
|
+
if (e.target === el && e.propertyName === "opacity") finish();
|
|
2042
|
+
};
|
|
2043
|
+
el.addEventListener("transitionend", onEnd);
|
|
2044
|
+
const timer = setTimeout(finish, transitionMs2(el) + EXIT_SLACK_MS2);
|
|
2045
|
+
return () => {
|
|
2046
|
+
done = true;
|
|
2047
|
+
el.removeEventListener("transitionend", onEnd);
|
|
2048
|
+
clearTimeout(timer);
|
|
2049
|
+
el.classList.remove("is-closing");
|
|
2050
|
+
};
|
|
2051
|
+
}, [open]);
|
|
2005
2052
|
const sx = search ? { ...SEARCH_DEFAULTS, ...strip(search === true ? {} : search) } : null;
|
|
2006
2053
|
const entries = sections?.length ? sections.flatMap((s) => s.items || []) : items || [];
|
|
2007
2054
|
const rows = entries.filter(isRow);
|
|
@@ -2012,7 +2059,7 @@ function Dropdown({
|
|
|
2012
2059
|
const given = rows.find((it) => it.selected);
|
|
2013
2060
|
const givenKey = given ? keyOf(given) : null;
|
|
2014
2061
|
const pickedKey = pick && pick.against === givenKey ? pick.key : null;
|
|
2015
|
-
|
|
2062
|
+
useEffect6(() => {
|
|
2016
2063
|
if (pick && pick.against !== givenKey) setPick(null);
|
|
2017
2064
|
}, [givenKey, pick]);
|
|
2018
2065
|
const picked = pickedKey != null ? rows.find((it) => keyOf(it) === pickedKey) : void 0;
|
|
@@ -2026,7 +2073,7 @@ function Dropdown({
|
|
|
2026
2073
|
const rowIds = /* @__PURE__ */ new Map();
|
|
2027
2074
|
rows.forEach((it, i) => rowIds.set(it, `${uid}-opt-${i}`));
|
|
2028
2075
|
const listId = `${uid}-list`;
|
|
2029
|
-
|
|
2076
|
+
useEffect6(() => {
|
|
2030
2077
|
if (!open) return;
|
|
2031
2078
|
const mine = () => close.current(false);
|
|
2032
2079
|
for (const other of [...openNow]) other();
|
|
@@ -2035,7 +2082,7 @@ function Dropdown({
|
|
|
2035
2082
|
openNow.delete(mine);
|
|
2036
2083
|
};
|
|
2037
2084
|
}, [open]);
|
|
2038
|
-
|
|
2085
|
+
useEffect6(() => {
|
|
2039
2086
|
if (!open) return;
|
|
2040
2087
|
const doc = root.current?.ownerDocument ?? document;
|
|
2041
2088
|
const onClick = (e) => {
|
|
@@ -2056,7 +2103,12 @@ function Dropdown({
|
|
|
2056
2103
|
useIsoLayoutEffect(() => {
|
|
2057
2104
|
const want = landOn.current;
|
|
2058
2105
|
landOn.current = null;
|
|
2059
|
-
if (!open)
|
|
2106
|
+
if (!open) {
|
|
2107
|
+
if (panel.current?.style && root.current?.closest(".ui-filter-bar") && !root.current.closest(".ui-filter-bar__chip")) {
|
|
2108
|
+
panel.current.style.minWidth = "";
|
|
2109
|
+
}
|
|
2110
|
+
return;
|
|
2111
|
+
}
|
|
2060
2112
|
if (sx) {
|
|
2061
2113
|
setQuery("");
|
|
2062
2114
|
const enabled = rows.filter((it) => !it.disabled);
|
|
@@ -2411,12 +2463,12 @@ function Segmented({ options, value, onChange, label, appearance = "pill", disab
|
|
|
2411
2463
|
}
|
|
2412
2464
|
|
|
2413
2465
|
// src/FilterBar.tsx
|
|
2414
|
-
import { useRef as
|
|
2415
|
-
import { filterChipText, filterChipName, filterChipUnset, focusNextStop } from "@apliteni/apliteni-ui";
|
|
2466
|
+
import { useRef as useRef11, useState as useState8 } from "react";
|
|
2467
|
+
import { filterChipText, filterChipName, filterChipUnset, filterChipItems, focusNextStop } from "@apliteni/apliteni-ui";
|
|
2416
2468
|
import { jsx as jsx24, jsxs as jsxs19 } from "react/jsx-runtime";
|
|
2417
2469
|
function FilterBar({ filters, label = "Filters", clearLabel = "Clear all filters", disabled = false, busy = false, onRemove, onClear, onChange }) {
|
|
2418
|
-
const bar =
|
|
2419
|
-
const focused =
|
|
2470
|
+
const bar = useRef11(null);
|
|
2471
|
+
const focused = useRef11(null);
|
|
2420
2472
|
const [opened, setOpened] = useState8({});
|
|
2421
2473
|
const blocked = disabled || busy;
|
|
2422
2474
|
useIsoLayoutEffect(() => {
|
|
@@ -2459,7 +2511,7 @@ function FilterBar({ filters, label = "Filters", clearLabel = "Clear all filters
|
|
|
2459
2511
|
/* @__PURE__ */ jsx24(
|
|
2460
2512
|
Dropdown,
|
|
2461
2513
|
{
|
|
2462
|
-
items: filter
|
|
2514
|
+
items: filterChipItems(filter),
|
|
2463
2515
|
variant: "select",
|
|
2464
2516
|
ariaLabel: filterChipName(filter),
|
|
2465
2517
|
triggerContent: /* @__PURE__ */ jsx24("span", { className: filterChipUnset(filter) ? "ui-dropdown__value is-placeholder" : "ui-dropdown__value", children: filterChipText(filter) }),
|
|
@@ -2522,13 +2574,13 @@ function DrawerSection({ title, headingLevel = 3, className, children, ...rest }
|
|
|
2522
2574
|
}
|
|
2523
2575
|
|
|
2524
2576
|
// src/Snippet.tsx
|
|
2525
|
-
import { useEffect as
|
|
2577
|
+
import { useEffect as useEffect7, useRef as useRef12, useState as useState9 } from "react";
|
|
2526
2578
|
import { Fragment as Fragment6, jsx as jsx26, jsxs as jsxs21 } from "react/jsx-runtime";
|
|
2527
2579
|
function Snippet({ label = "shell", code = "", children, copy = true, reveal = false, copyLabel = "Copy code" }) {
|
|
2528
2580
|
const [status, setStatus] = useState9("idle");
|
|
2529
|
-
const request =
|
|
2530
|
-
const timer =
|
|
2531
|
-
|
|
2581
|
+
const request = useRef12(0);
|
|
2582
|
+
const timer = useRef12(void 0);
|
|
2583
|
+
useEffect7(() => {
|
|
2532
2584
|
setStatus("idle");
|
|
2533
2585
|
return () => {
|
|
2534
2586
|
request.current++;
|
|
@@ -2571,15 +2623,15 @@ function Snippet({ label = "shell", code = "", children, copy = true, reveal = f
|
|
|
2571
2623
|
}
|
|
2572
2624
|
|
|
2573
2625
|
// src/Tabs.tsx
|
|
2574
|
-
import { useEffect as
|
|
2626
|
+
import { useEffect as useEffect8, useId as useId8, useRef as useRef13 } from "react";
|
|
2575
2627
|
import { playEntrance } from "@apliteni/apliteni-ui/motion";
|
|
2576
2628
|
import { Fragment as Fragment7, jsx as jsx27, jsxs as jsxs22 } from "react/jsx-runtime";
|
|
2577
2629
|
function Tabs({ items, value, onChange, label }) {
|
|
2578
|
-
const id =
|
|
2579
|
-
const previous =
|
|
2580
|
-
const activePanel =
|
|
2581
|
-
const buttons =
|
|
2582
|
-
|
|
2630
|
+
const id = useId8();
|
|
2631
|
+
const previous = useRef13(value);
|
|
2632
|
+
const activePanel = useRef13(null);
|
|
2633
|
+
const buttons = useRef13([]);
|
|
2634
|
+
useEffect8(() => {
|
|
2583
2635
|
if (previous.current !== value) playEntrance(activePanel.current);
|
|
2584
2636
|
previous.current = value;
|
|
2585
2637
|
}, [value]);
|
|
@@ -2648,14 +2700,14 @@ function Tabs({ items, value, onChange, label }) {
|
|
|
2648
2700
|
}
|
|
2649
2701
|
|
|
2650
2702
|
// src/Timeline.tsx
|
|
2651
|
-
import { useEffect as
|
|
2703
|
+
import { useEffect as useEffect9, useRef as useRef14 } from "react";
|
|
2652
2704
|
import { playEntrance as playEntrance2, prefersReducedMotion } from "@apliteni/apliteni-ui/motion";
|
|
2653
2705
|
import { jsx as jsx28, jsxs as jsxs23 } from "react/jsx-runtime";
|
|
2654
2706
|
var kindIcons = { person: "user", rule: "bolt", reversal: "refresh" };
|
|
2655
2707
|
function Timeline({ events, className, ...rest }) {
|
|
2656
|
-
const previousIds =
|
|
2657
|
-
const rows =
|
|
2658
|
-
|
|
2708
|
+
const previousIds = useRef14(new Set(events.map((event) => event.id)));
|
|
2709
|
+
const rows = useRef14(/* @__PURE__ */ new Map());
|
|
2710
|
+
useEffect9(() => {
|
|
2659
2711
|
if (previousIds.current.size > 0 && !prefersReducedMotion()) {
|
|
2660
2712
|
for (const event of events) {
|
|
2661
2713
|
if (previousIds.current.has(event.id)) continue;
|
|
@@ -2698,7 +2750,7 @@ function Timeline({ events, className, ...rest }) {
|
|
|
2698
2750
|
}
|
|
2699
2751
|
|
|
2700
2752
|
// src/Field.tsx
|
|
2701
|
-
import { forwardRef as forwardRef6, useEffect as
|
|
2753
|
+
import { forwardRef as forwardRef6, useEffect as useEffect10, useId as useId9, useRef as useRef15, useState as useState10 } from "react";
|
|
2702
2754
|
import { esc, icon as icon4 } from "@apliteni/apliteni-ui";
|
|
2703
2755
|
import { jsx as jsx29, jsxs as jsxs24 } from "react/jsx-runtime";
|
|
2704
2756
|
function messages(id, hint, error) {
|
|
@@ -2727,12 +2779,12 @@ function wiring(id, hint, error) {
|
|
|
2727
2779
|
return { id, "aria-describedby": messages(id, hint, error), "aria-invalid": error ? true : void 0 };
|
|
2728
2780
|
}
|
|
2729
2781
|
function Field({ id: suppliedId, label, hint, error, required, children }) {
|
|
2730
|
-
const generatedId =
|
|
2782
|
+
const generatedId = useId9();
|
|
2731
2783
|
const id = suppliedId ?? generatedId;
|
|
2732
2784
|
return /* @__PURE__ */ jsx29(Frame, { ...{ id, label, hint, error, required }, children: children({ ...wiring(id, hint, error), required }) });
|
|
2733
2785
|
}
|
|
2734
2786
|
var TextField = forwardRef6(function TextField2({ label, hint, error, required, type = "text", unit, icon: glyph, className = "", ...props }, ref) {
|
|
2735
|
-
const id =
|
|
2787
|
+
const id = useId9();
|
|
2736
2788
|
const hasUnit = type === "number" && !!unit;
|
|
2737
2789
|
const describedBy = [messages(id, hint, error), hasUnit ? `${id}-unit` : ""].filter(Boolean).join(" ") || void 0;
|
|
2738
2790
|
const control = /* @__PURE__ */ jsx29(
|
|
@@ -2759,7 +2811,7 @@ var TextField = forwardRef6(function TextField2({ label, hint, error, required,
|
|
|
2759
2811
|
] }) : control });
|
|
2760
2812
|
});
|
|
2761
2813
|
function TextArea({ label, hint, error, required, className = "", ...props }) {
|
|
2762
|
-
const id =
|
|
2814
|
+
const id = useId9();
|
|
2763
2815
|
return /* @__PURE__ */ jsx29(Frame, { ...{ id, label, hint, error, required }, children: /* @__PURE__ */ jsx29(
|
|
2764
2816
|
"textarea",
|
|
2765
2817
|
{
|
|
@@ -2772,7 +2824,7 @@ function TextArea({ label, hint, error, required, className = "", ...props }) {
|
|
|
2772
2824
|
) });
|
|
2773
2825
|
}
|
|
2774
2826
|
function SelectField({ label, hint, error, required, className = "", children, ...props }) {
|
|
2775
|
-
const id =
|
|
2827
|
+
const id = useId9();
|
|
2776
2828
|
return /* @__PURE__ */ jsx29(Frame, { ...{ id, label, hint, error, required }, children: /* @__PURE__ */ jsx29(
|
|
2777
2829
|
"select",
|
|
2778
2830
|
{
|
|
@@ -2785,11 +2837,11 @@ function SelectField({ label, hint, error, required, className = "", children, .
|
|
|
2785
2837
|
) });
|
|
2786
2838
|
}
|
|
2787
2839
|
function FileField({ label, hint, error, required, disabled, onFileChange, className = "", ...props }) {
|
|
2788
|
-
const id =
|
|
2789
|
-
const input =
|
|
2840
|
+
const id = useId9();
|
|
2841
|
+
const input = useRef15(null);
|
|
2790
2842
|
const [file, setFile] = useState10(null);
|
|
2791
2843
|
const [dragging, setDragging] = useState10(false);
|
|
2792
|
-
|
|
2844
|
+
useEffect10(() => {
|
|
2793
2845
|
const form = input.current?.form;
|
|
2794
2846
|
const reset = () => {
|
|
2795
2847
|
setFile(null);
|
|
@@ -2848,7 +2900,7 @@ function FileField({ label, hint, error, required, disabled, onFileChange, class
|
|
|
2848
2900
|
}
|
|
2849
2901
|
|
|
2850
2902
|
// src/FileDrop.tsx
|
|
2851
|
-
import { useRef as
|
|
2903
|
+
import { useRef as useRef16, useState as useState11 } from "react";
|
|
2852
2904
|
import { Fragment as Fragment8, jsx as jsx30, jsxs as jsxs25 } from "react/jsx-runtime";
|
|
2853
2905
|
var MARKS = {
|
|
2854
2906
|
uploading: "clock",
|
|
@@ -2882,7 +2934,7 @@ function FileDrop({
|
|
|
2882
2934
|
children,
|
|
2883
2935
|
className
|
|
2884
2936
|
}) {
|
|
2885
|
-
const input =
|
|
2937
|
+
const input = useRef16(null);
|
|
2886
2938
|
const [over, setOver] = useState11(false);
|
|
2887
2939
|
const status = file?.status ?? "uploading";
|
|
2888
2940
|
const word = status === "error" ? file?.error ?? WORDS.error : file?.state ?? WORDS[status];
|
|
@@ -3017,7 +3069,7 @@ var SearchField = forwardRef7(function SearchField2({ ariaLabel, className, ...p
|
|
|
3017
3069
|
});
|
|
3018
3070
|
|
|
3019
3071
|
// src/Toast.tsx
|
|
3020
|
-
import { createContext as createContext2, useCallback as useCallback4, useContext as useContext2, useEffect as
|
|
3072
|
+
import { createContext as createContext2, useCallback as useCallback4, useContext as useContext2, useEffect as useEffect11, useLayoutEffect as useLayoutEffect4, useRef as useRef17, useState as useState12 } from "react";
|
|
3021
3073
|
import { createPortal as createPortal4 } from "react-dom";
|
|
3022
3074
|
import { jsx as jsx32, jsxs as jsxs27 } from "react/jsx-runtime";
|
|
3023
3075
|
var Context = createContext2(null);
|
|
@@ -3028,10 +3080,10 @@ function useToast() {
|
|
|
3028
3080
|
return push;
|
|
3029
3081
|
}
|
|
3030
3082
|
function Notice({ notice, remove }) {
|
|
3031
|
-
const root =
|
|
3083
|
+
const root = useRef17(null);
|
|
3032
3084
|
const [leaving2, setLeaving] = useState12(false);
|
|
3033
|
-
const dismissed =
|
|
3034
|
-
const remaining =
|
|
3085
|
+
const dismissed = useRef17(false);
|
|
3086
|
+
const remaining = useRef17(5e3);
|
|
3035
3087
|
const [hovered, setHovered] = useState12(false);
|
|
3036
3088
|
const [focused, setFocused] = useState12(false);
|
|
3037
3089
|
const paused = hovered || focused;
|
|
@@ -3042,7 +3094,7 @@ function Notice({ notice, remove }) {
|
|
|
3042
3094
|
if (typeof matchMedia === "function" && matchMedia("(prefers-reduced-motion: reduce)").matches) remove();
|
|
3043
3095
|
else setLeaving(true);
|
|
3044
3096
|
}, [remove]);
|
|
3045
|
-
|
|
3097
|
+
useEffect11(() => {
|
|
3046
3098
|
if (leaving2) {
|
|
3047
3099
|
const element = root.current;
|
|
3048
3100
|
const finish = (event) => {
|
|
@@ -3056,7 +3108,7 @@ function Notice({ notice, remove }) {
|
|
|
3056
3108
|
};
|
|
3057
3109
|
}
|
|
3058
3110
|
}, [leaving2, remove]);
|
|
3059
|
-
|
|
3111
|
+
useEffect11(() => {
|
|
3060
3112
|
if (leaving2 || notice.action || paused) return;
|
|
3061
3113
|
const started = Date.now();
|
|
3062
3114
|
const timer = setTimeout(dismiss, remaining.current);
|
|
@@ -3134,8 +3186,8 @@ function usePublishedReach(stack2, count) {
|
|
|
3134
3186
|
}
|
|
3135
3187
|
function Toast({ children }) {
|
|
3136
3188
|
const [entries, setEntries] = useState12([]);
|
|
3137
|
-
const nextId =
|
|
3138
|
-
const stack2 =
|
|
3189
|
+
const nextId = useRef17(0);
|
|
3190
|
+
const stack2 = useRef17(null);
|
|
3139
3191
|
const push = useCallback4((notice) => {
|
|
3140
3192
|
const id = nextId.current++;
|
|
3141
3193
|
const remove = () => setEntries((current) => current.filter((entry) => entry.id !== id));
|
|
@@ -3152,22 +3204,22 @@ function Toast({ children }) {
|
|
|
3152
3204
|
}
|
|
3153
3205
|
|
|
3154
3206
|
// src/AppShell.tsx
|
|
3155
|
-
import { useEffect as
|
|
3207
|
+
import { useEffect as useEffect13, useId as useId11, useState as useState14 } from "react";
|
|
3156
3208
|
import { applyTheme, paletteHotkey, prism, railCollapsed, themeIcon, themeName } from "@apliteni/apliteni-ui";
|
|
3157
3209
|
|
|
3158
3210
|
// src/SidebarNav.tsx
|
|
3159
|
-
import { forwardRef as forwardRef8, useEffect as
|
|
3211
|
+
import { forwardRef as forwardRef8, useEffect as useEffect12, useId as useId10, useState as useState13 } from "react";
|
|
3160
3212
|
import { ENTRANCE_FALLBACK_MS } from "@apliteni/apliteni-ui/motion";
|
|
3161
3213
|
import { Fragment as Fragment9, jsx as jsx33, jsxs as jsxs28 } from "react/jsx-runtime";
|
|
3162
3214
|
var badgeText = (badge) => typeof badge === "object" ? badge.text : badge;
|
|
3163
3215
|
function Row({ item, active, activeIs, collapsed, renderLink, sub }) {
|
|
3164
|
-
const uid =
|
|
3216
|
+
const uid = useId10();
|
|
3165
3217
|
const group = !sub && !!item.items?.length;
|
|
3166
3218
|
const childActive = group && item.items.some((child) => child.id === active);
|
|
3167
3219
|
const [expanded, setExpanded] = useState13();
|
|
3168
3220
|
const open = expanded ?? item.defaultOpen ?? childActive;
|
|
3169
3221
|
const [entering, setEntering] = useState13(false);
|
|
3170
|
-
|
|
3222
|
+
useEffect12(() => {
|
|
3171
3223
|
if (!entering) return;
|
|
3172
3224
|
const timer = setTimeout(() => setEntering(false), ENTRANCE_FALLBACK_MS);
|
|
3173
3225
|
return () => clearTimeout(timer);
|
|
@@ -3218,7 +3270,7 @@ function Row({ item, active, activeIs, collapsed, renderLink, sub }) {
|
|
|
3218
3270
|
return /* @__PURE__ */ jsx33("li", { children: renderLink ? renderLink(item, props) : /* @__PURE__ */ jsx33("a", { ...props }) });
|
|
3219
3271
|
}
|
|
3220
3272
|
function Section({ section, ...props }) {
|
|
3221
|
-
const uid =
|
|
3273
|
+
const uid = useId10();
|
|
3222
3274
|
return /* @__PURE__ */ jsxs28("div", { className: "ui-nav__section", children: [
|
|
3223
3275
|
section.label && /* @__PURE__ */ jsx33("div", { className: "ui-nav__cap", id: uid, "aria-hidden": props.collapsed || void 0, children: section.label }),
|
|
3224
3276
|
/* @__PURE__ */ jsx33("ul", { className: "ui-nav__list", "aria-labelledby": section.label ? uid : void 0, children: section.items.map((item) => /* @__PURE__ */ jsx33(Row, { item, ...props }, item.id)) })
|
|
@@ -3274,21 +3326,21 @@ function AppShell({
|
|
|
3274
3326
|
palette,
|
|
3275
3327
|
renderLink
|
|
3276
3328
|
}) {
|
|
3277
|
-
const uid =
|
|
3329
|
+
const uid = useId11();
|
|
3278
3330
|
const [collapsed, setCollapsed] = useState14(defaultCollapsed);
|
|
3279
3331
|
const [search, setSearch] = useState14(false);
|
|
3280
3332
|
const [more, setMore] = useState14(false);
|
|
3281
3333
|
const [theme, setTheme] = useState14("dark");
|
|
3282
3334
|
const current = path(pathname);
|
|
3283
3335
|
const active = sections.filter((section) => current === path(section.href) || path(section.href) !== "/" && current.startsWith(`${path(section.href)}/`)).sort((a, b) => path(b.href).length - path(a.href).length)[0];
|
|
3284
|
-
|
|
3336
|
+
useEffect13(() => {
|
|
3285
3337
|
setCollapsed(railCollapsed() ?? defaultCollapsed);
|
|
3286
3338
|
}, [defaultCollapsed]);
|
|
3287
|
-
|
|
3339
|
+
useEffect13(() => {
|
|
3288
3340
|
setMore(false);
|
|
3289
3341
|
setSearch(false);
|
|
3290
3342
|
}, [pathname]);
|
|
3291
|
-
|
|
3343
|
+
useEffect13(() => {
|
|
3292
3344
|
const root = document.documentElement;
|
|
3293
3345
|
const sync2 = () => setTheme(root.getAttribute("data-theme") === "light" ? "light" : "dark");
|
|
3294
3346
|
sync2();
|
|
@@ -3296,7 +3348,7 @@ function AppShell({
|
|
|
3296
3348
|
observer.observe(root, { attributes: true, attributeFilter: ["data-theme"] });
|
|
3297
3349
|
return () => observer.disconnect();
|
|
3298
3350
|
}, []);
|
|
3299
|
-
|
|
3351
|
+
useEffect13(() => {
|
|
3300
3352
|
const hotkey = (event) => {
|
|
3301
3353
|
if (!(event.metaKey || event.ctrlKey) || event.altKey || event.shiftKey || event.key.toLowerCase() !== "k") return;
|
|
3302
3354
|
const target = event.target;
|
|
@@ -3523,7 +3575,7 @@ function ThemeToggle({ labelled = false }) {
|
|
|
3523
3575
|
}
|
|
3524
3576
|
|
|
3525
3577
|
// src/Success.tsx
|
|
3526
|
-
import { forwardRef as forwardRef9, useEffect as
|
|
3578
|
+
import { forwardRef as forwardRef9, useEffect as useEffect14, useRef as useRef18, useState as useState15 } from "react";
|
|
3527
3579
|
import { jsx as jsx37, jsxs as jsxs32 } from "react/jsx-runtime";
|
|
3528
3580
|
var SuccessCheck = forwardRef9(function SuccessCheck2({ variant = "line", className, ...rest }, ref) {
|
|
3529
3581
|
const circled = variant === "circled";
|
|
@@ -3571,11 +3623,11 @@ var pieces = [
|
|
|
3571
3623
|
];
|
|
3572
3624
|
function Countdown({ seconds, label = "Redirecting", onDone }) {
|
|
3573
3625
|
const [remaining, setRemaining] = useState15(seconds);
|
|
3574
|
-
const callback =
|
|
3575
|
-
|
|
3626
|
+
const callback = useRef18(onDone);
|
|
3627
|
+
useEffect14(() => {
|
|
3576
3628
|
callback.current = onDone;
|
|
3577
3629
|
}, [onDone]);
|
|
3578
|
-
|
|
3630
|
+
useEffect14(() => {
|
|
3579
3631
|
let count = seconds;
|
|
3580
3632
|
const timer = setInterval(() => {
|
|
3581
3633
|
count -= 1;
|
|
@@ -16,6 +16,8 @@ export { dropdownMatch, dropdownFiltering } from '../logic/dropdown.js';
|
|
|
16
16
|
// wireDropdown(container); // or let wireTopbar() do it
|
|
17
17
|
import { esc, icon } from './index.js';
|
|
18
18
|
import { safeUrl } from '../html.js';
|
|
19
|
+
// The fade's own length, read off the stylesheet rather than copied. why: src/motion.js
|
|
20
|
+
import { transitionMs } from '../motion.js';
|
|
19
21
|
const cx = (...a) => a.filter(Boolean).join(' ');
|
|
20
22
|
|
|
21
23
|
// A trailing status badge. `badge` is the text shown, in the case it is written
|
|
@@ -325,6 +327,215 @@ function ddResetSearch(dd, panel, search) {
|
|
|
325
327
|
if (panel.offsetWidth) panel.style.minWidth = `${panel.offsetWidth}px`;
|
|
326
328
|
}
|
|
327
329
|
|
|
330
|
+
/* The kit's menu floor, the one `.ui-dropdown__panel` writes. The stylesheets
|
|
331
|
+
* carry the same number; stories/filter-bar-fit.test.js reads all of them and
|
|
332
|
+
* fails when one drifts. why: src/styles/dropdown.css */
|
|
333
|
+
export const DD_MENU_FLOOR = 240;
|
|
334
|
+
|
|
335
|
+
/**
|
|
336
|
+
* Where a filter chip's menu may sit. #484 bounds the panel to its trigger, so a
|
|
337
|
+
* chip printing its value alone (#536) left a 48px menu breaking words
|
|
338
|
+
* mid-letter — #549. An OPEN panel takes the floor instead and slides along the
|
|
339
|
+
* row when the room on the side it opens from is short; shut, it keeps the
|
|
340
|
+
* trigger's width, which is why #467 needs no measuring at all.
|
|
341
|
+
*
|
|
342
|
+
* A panel is anchored at one edge of its dropdown — its inline end when it
|
|
343
|
+
* carries `is-end` — so room is measured from that edge and the slide goes the
|
|
344
|
+
* other way. Measuring an end-anchored panel forwards put one off the page.
|
|
345
|
+
* why: docs/specification.md#a-filter-row-holds-its-panels
|
|
346
|
+
*
|
|
347
|
+
* @param {Element} dd a `.ui-dropdown` that may be inside a filter row
|
|
348
|
+
* @param {number} [floor] the width to reach for
|
|
349
|
+
* @returns {{room: number, shift: number, floor: number, end: boolean}|null}
|
|
350
|
+
*/
|
|
351
|
+
export function filterPanelFit(dd, floor = DD_MENU_FLOOR) {
|
|
352
|
+
/* A chip's menu, not any menu in a filter row: the slide below is measured from
|
|
353
|
+
* the trigger's offset along the row, so a panel anchored to the row itself is
|
|
354
|
+
* not this function's subject and must size itself. why: src/styles/filter-bar.css */
|
|
355
|
+
const chip = typeof dd?.closest === 'function' ? dd.closest('.ui-filter-bar__chip') : null;
|
|
356
|
+
const bar = chip ? chip.closest('.ui-filter-bar') : null;
|
|
357
|
+
if (!bar || typeof bar.getBoundingClientRect !== 'function') return null;
|
|
358
|
+
const row = bar.getBoundingClientRect();
|
|
359
|
+
const box = dd.getBoundingClientRect();
|
|
360
|
+
const panel = typeof dd.querySelector === 'function' ? dd.querySelector('.ui-dropdown__panel') : null;
|
|
361
|
+
const end = !!panel?.classList?.contains('is-end');
|
|
362
|
+
// A row narrower than the floor decides the width; nothing may leave the row.
|
|
363
|
+
const want = Math.min(floor, row.width);
|
|
364
|
+
const ahead = end
|
|
365
|
+
? Math.max(0, box.right - row.left) // an end-anchored panel grows backwards
|
|
366
|
+
: Math.max(0, row.right - box.left);
|
|
367
|
+
const slack = end
|
|
368
|
+
? Math.max(0, row.right - box.right) // …and slides towards the row's end
|
|
369
|
+
: Math.max(0, box.left - row.left);
|
|
370
|
+
const shift = Math.min(slack, Math.max(0, want - ahead));
|
|
371
|
+
return { room: ahead + shift, shift, floor: want, end };
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
/** Write the fit onto the panel, and say whether any of the three numbers moved.
|
|
375
|
+
* All three, because the stylesheet reads all three; it falls back to the
|
|
376
|
+
* trigger's width when they are unset, so a panel opened before this runs is
|
|
377
|
+
* bounded rather than unbounded.
|
|
378
|
+
*
|
|
379
|
+
* A number that has not changed is not written again, which is what stops the
|
|
380
|
+
* observers below from answering their own writes: a fit landing on the same
|
|
381
|
+
* three numbers mutates nothing, so it reports nothing. */
|
|
382
|
+
function ddWriteFit(panel, fit) {
|
|
383
|
+
let moved = false;
|
|
384
|
+
for (const prop of ['room', 'shift', 'floor']) {
|
|
385
|
+
const name = `--ui-filter-panel-${prop}`;
|
|
386
|
+
const next = `${fit[prop]}px`;
|
|
387
|
+
if (panel.style.getPropertyValue(name) === next) continue;
|
|
388
|
+
panel.style.setProperty(name, next);
|
|
389
|
+
moved = true;
|
|
390
|
+
}
|
|
391
|
+
return moved;
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
/** Fit a panel just opened, or leave it alone outside a filter row. */
|
|
395
|
+
function ddFitFilterPanel(dd, panel) {
|
|
396
|
+
const fit = filterPanelFit(dd);
|
|
397
|
+
if (!fit || !panel?.style) return false;
|
|
398
|
+
return ddWriteFit(panel, fit);
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
/** Measure an open menu again where its row now puts it. The fit it was opened
|
|
402
|
+
* with can describe a row that no longer exists — a menu opened at 1280 and left
|
|
403
|
+
* open at 390 kept a 1102px room and stood 11px off the page — or a place in that
|
|
404
|
+
* row the chip has since left. A search panel's inline `min-width` was read at
|
|
405
|
+
* the old width too, and inline beats the sheet, so it is read again off the
|
|
406
|
+
* newly bounded box; only when the fit moved, because re-reading it is itself a
|
|
407
|
+
* write.
|
|
408
|
+
* why: docs/specification.md#a-filter-row-holds-its-panels */
|
|
409
|
+
function ddRefitFilterPanel(dd, panel) {
|
|
410
|
+
const fit = panel?.style ? filterPanelFit(dd) : null;
|
|
411
|
+
if (!fit || !ddWriteFit(panel, fit)) return;
|
|
412
|
+
if (!panel.style.minWidth) return;
|
|
413
|
+
panel.style.minWidth = '';
|
|
414
|
+
if (panel.offsetWidth) panel.style.minWidth = `${panel.offsetWidth}px`;
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
/** The row this chip's menu is fitted to, or null outside one. */
|
|
418
|
+
function ddRowOf(dd) {
|
|
419
|
+
return dd?.closest?.('.ui-filter-bar__chip')?.closest('.ui-filter-bar') || null;
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
/** Is this node inside some dropdown panel? The fit writes to a panel and to
|
|
423
|
+
* nothing else, so a change inside one is never news — and a panel is
|
|
424
|
+
* `position: absolute`, so nothing in it can move a chip along its row. Any
|
|
425
|
+
* panel, not only the one being fitted: two open menus in one row would
|
|
426
|
+
* otherwise answer each other a mutation at a time.
|
|
427
|
+
* why: docs/specification.md#a-filter-row-holds-its-panels */
|
|
428
|
+
function ddOurs(node) {
|
|
429
|
+
const el = node?.nodeType === 1 ? node : node?.parentElement;
|
|
430
|
+
return !!el?.closest?.('[data-dropdown-panel]');
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
/**
|
|
434
|
+
* Watch what the fit is measured from, and fit the menu again when it changes.
|
|
435
|
+
*
|
|
436
|
+
* Two inputs, two watchers. The row's width is one, observed rather than taken
|
|
437
|
+
* from `resize`, which fires before an animating row has settled. Where the chip
|
|
438
|
+
* sits along the row is the other, and it moves while the row keeps both of its
|
|
439
|
+
* dimensions: re-labelling the chip in front of an open menu at 390px slid this
|
|
440
|
+
* one 27.97px and took the menu 27.97px out of its row. So the boxes laid out in
|
|
441
|
+
* the row are observed too — a chip is moved by the size of the ones in front of
|
|
442
|
+
* it — and the mutation that moved it is watched for the frame before any of it
|
|
443
|
+
* is laid out.
|
|
444
|
+
*
|
|
445
|
+
* Why those two cover the arithmetic, and the one case they do not:
|
|
446
|
+
* why: docs/specification.md#a-filter-row-holds-its-panels
|
|
447
|
+
*/
|
|
448
|
+
function ddWatchRow(dd, panel) {
|
|
449
|
+
ddUnwatchRow(dd);
|
|
450
|
+
const row = ddRowOf(dd);
|
|
451
|
+
const view = dd.ownerDocument?.defaultView;
|
|
452
|
+
if (!row) return;
|
|
453
|
+
const refit = () => { if (dd.classList.contains('open')) ddRefitFilterPanel(dd, panel); };
|
|
454
|
+
if (typeof view?.ResizeObserver === 'function') {
|
|
455
|
+
// observe() reports each current box at once, which the fit was just written
|
|
456
|
+
// from, so the first round writes nothing.
|
|
457
|
+
const ro = new view.ResizeObserver(refit);
|
|
458
|
+
ro.observe(row);
|
|
459
|
+
for (const kid of row.children) ro.observe(kid);
|
|
460
|
+
dd.__ddRowFit = ro;
|
|
461
|
+
}
|
|
462
|
+
if (typeof view?.MutationObserver !== 'function') return;
|
|
463
|
+
const mo = new view.MutationObserver((records) => {
|
|
464
|
+
if (records.every((m) => ddOurs(m.target))) return;
|
|
465
|
+
// A chip added or removed brings a box the observer above is not watching yet.
|
|
466
|
+
if (dd.__ddRowFit && records.some((m) => m.type === 'childList')) {
|
|
467
|
+
for (const kid of row.children) dd.__ddRowFit.observe(kid);
|
|
468
|
+
}
|
|
469
|
+
refit();
|
|
470
|
+
});
|
|
471
|
+
/* characterData as well as the two structural kinds: a chip's printed value
|
|
472
|
+
replaced is a text node changed, with no child list and no attribute moving
|
|
473
|
+
with it — which is what a React re-render of that value arrives as too.
|
|
474
|
+
Attributes, because a class is how a chip changes shape; subtree, because the
|
|
475
|
+
text that sets a chip's width is inside it. */
|
|
476
|
+
mo.observe(row, { subtree: true, childList: true, attributes: true, characterData: true });
|
|
477
|
+
dd.__ddRowMo = mo;
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
function ddUnwatchRow(dd) {
|
|
481
|
+
dd.__ddRowFit?.disconnect();
|
|
482
|
+
dd.__ddRowFit = null;
|
|
483
|
+
dd.__ddRowMo?.disconnect();
|
|
484
|
+
dd.__ddRowMo = null;
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
/* A few frames past the sheet's own end, so the timer never cuts the fade's last
|
|
488
|
+
* frame. The number the dialogs' exit uses. */
|
|
489
|
+
const DD_EXIT_SLACK_MS = 50;
|
|
490
|
+
|
|
491
|
+
/**
|
|
492
|
+
* Hold a closing chip menu's open geometry until its fade has finished.
|
|
493
|
+
*
|
|
494
|
+
* `.open` stops matching in the frame the menu closes; the fade does not — the
|
|
495
|
+
* panel transitions opacity over --dur-med. Dropping the width there left a 240px
|
|
496
|
+
* menu painted as a 48px column of single letters: #549 on the way out. The timer
|
|
497
|
+
* is the way out of a transitionend that never comes.
|
|
498
|
+
* why: src/styles/filter-bar.css
|
|
499
|
+
*/
|
|
500
|
+
function ddHoldClose(dd, panel) {
|
|
501
|
+
ddCancelClose(dd, panel);
|
|
502
|
+
panel.classList.add('is-closing');
|
|
503
|
+
let done = false;
|
|
504
|
+
const finish = () => {
|
|
505
|
+
if (done) return;
|
|
506
|
+
done = true;
|
|
507
|
+
dd.__ddClosing = null;
|
|
508
|
+
panel.classList.remove('is-closing');
|
|
509
|
+
panel.style.minWidth = '';
|
|
510
|
+
for (const prop of ['room', 'shift', 'floor']) {
|
|
511
|
+
panel.style.removeProperty(`--ui-filter-panel-${prop}`);
|
|
512
|
+
}
|
|
513
|
+
};
|
|
514
|
+
// Only the panel's own fade: a row's background transition bubbles here too.
|
|
515
|
+
const onEnd = (e) => { if (e.target === panel && e.propertyName === 'opacity') stop(); };
|
|
516
|
+
const timer = setTimeout(() => stop(), transitionMs(panel) + DD_EXIT_SLACK_MS);
|
|
517
|
+
function stop() {
|
|
518
|
+
panel.removeEventListener('transitionend', onEnd);
|
|
519
|
+
clearTimeout(timer);
|
|
520
|
+
finish();
|
|
521
|
+
}
|
|
522
|
+
panel.addEventListener('transitionend', onEnd);
|
|
523
|
+
// Re-opened mid-fade: the wait is abandoned, so it cannot clear a fit the new
|
|
524
|
+
// open has already written.
|
|
525
|
+
dd.__ddClosing = () => {
|
|
526
|
+
done = true;
|
|
527
|
+
dd.__ddClosing = null;
|
|
528
|
+
panel.removeEventListener('transitionend', onEnd);
|
|
529
|
+
clearTimeout(timer);
|
|
530
|
+
panel.classList.remove('is-closing');
|
|
531
|
+
};
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
function ddCancelClose(dd, panel) {
|
|
535
|
+
if (dd.__ddClosing) dd.__ddClosing();
|
|
536
|
+
else panel?.classList?.remove('is-closing');
|
|
537
|
+
}
|
|
538
|
+
|
|
328
539
|
// `auto` is the only direction the wiring decides; `up` and the default are the
|
|
329
540
|
// panel's own class, set once at render. Flip only when below is too tight AND
|
|
330
541
|
// above is roomier, so a panel that fits nowhere still opens the way it says.
|
|
@@ -373,7 +584,17 @@ function sweepOrphanPanels(host) {
|
|
|
373
584
|
function closeDropdown(dd) {
|
|
374
585
|
if (!dd.classList.contains('open')) return;
|
|
375
586
|
dd.classList.remove('open');
|
|
376
|
-
ddPanelOf(dd)
|
|
587
|
+
const panel = ddPanelOf(dd);
|
|
588
|
+
panel?.classList.remove('is-open');
|
|
589
|
+
ddUnwatchRow(dd);
|
|
590
|
+
/* A search panel carries the width it was opened at as an inline `min-width`,
|
|
591
|
+
* and inline beats any sheet. Inside a filter row that leaves a SHUT panel wider
|
|
592
|
+
* than its trigger, which is #467. Cleared here, and ddResetSearch() writes it
|
|
593
|
+
* again on the next open — except for a chip's menu, which gives the pin and the
|
|
594
|
+
* fit back at the END of its fade, both being its open geometry. A portalled
|
|
595
|
+
* panel is never fitted, so it has none to hold. why: ddHoldClose() */
|
|
596
|
+
if (panel?.style && !dd.__ddPanel && ddRowOf(dd)) ddHoldClose(dd, panel);
|
|
597
|
+
else if (panel?.style && dd.closest?.('.ui-filter-bar')) panel.style.minWidth = '';
|
|
377
598
|
dd.querySelector('[data-dropdown-trigger]')?.setAttribute('aria-expanded', 'false');
|
|
378
599
|
}
|
|
379
600
|
|
|
@@ -396,12 +617,21 @@ function openDropdown(dd, focusIdx) {
|
|
|
396
617
|
closeAllDropdowns(dd);
|
|
397
618
|
const panel = ddPanelOf(dd);
|
|
398
619
|
const search = ddSearchOf(dd);
|
|
399
|
-
if (panel && search) ddResetSearch(dd, panel, search);
|
|
400
620
|
if (panel) {
|
|
621
|
+
// An earlier close may still be holding its geometry; this open owns it now.
|
|
622
|
+
ddCancelClose(dd, panel);
|
|
401
623
|
ddResolveDirection(dd, panel);
|
|
402
624
|
if (dd.__ddPanel) { positionPortalPanel(dd, panel); panel.classList.add('is-open'); }
|
|
625
|
+
else { ddFitFilterPanel(dd, panel); ddWatchRow(dd, panel); }
|
|
403
626
|
}
|
|
404
627
|
dd.classList.add('open');
|
|
628
|
+
/* After `open`, and after the fit: ddResetSearch() pins the width it reads off
|
|
629
|
+
* the panel, and the rule that widens a filter menu only applies once the
|
|
630
|
+
* panel is open. Reading first pinned 50px inline, which beats any stylesheet
|
|
631
|
+
* — a searchable chip in a filter row kept the 74px menu #549 reported, while
|
|
632
|
+
* React, whose effects already ran in this order, gave 240px for the same
|
|
633
|
+
* markup. why: src/styles/filter-bar.css */
|
|
634
|
+
if (panel && search) ddResetSearch(dd, panel, search);
|
|
405
635
|
dd.querySelector('[data-dropdown-trigger]')?.setAttribute('aria-expanded', 'true');
|
|
406
636
|
// With search, focus goes to the field however the panel was opened, and
|
|
407
637
|
// the selected row (or the first) is the one Enter would pick.
|
|
@@ -451,10 +681,21 @@ function ddListen(doc) {
|
|
|
451
681
|
if (dd.classList.contains('open') && dd.__ddPanel) positionPortalPanel(dd, dd.__ddPanel);
|
|
452
682
|
}
|
|
453
683
|
};
|
|
684
|
+
/* The fallback re-fit, for a view with no ResizeObserver. Not taken as well where
|
|
685
|
+
* ddWatchRow() is watching: a `resize` event is the worse measurement of the two.
|
|
686
|
+
* why: docs/specification.md#a-filter-row-holds-its-panels */
|
|
687
|
+
const refit = () => {
|
|
688
|
+
for (const dd of ddLive()) {
|
|
689
|
+
if (dd.classList.contains('open') && !dd.__ddPanel && !dd.__ddRowFit) {
|
|
690
|
+
ddRefitFilterPanel(dd, ddPanelOf(dd));
|
|
691
|
+
}
|
|
692
|
+
}
|
|
693
|
+
};
|
|
454
694
|
const view = doc.defaultView;
|
|
455
695
|
if (!view) return;
|
|
456
696
|
view.addEventListener('scroll', reposition, true);
|
|
457
697
|
view.addEventListener('resize', reposition);
|
|
698
|
+
view.addEventListener('resize', refit);
|
|
458
699
|
}
|
|
459
700
|
|
|
460
701
|
export function wireDropdown(root = document) {
|
|
@@ -489,6 +730,15 @@ export function wireDropdown(root = document) {
|
|
|
489
730
|
}
|
|
490
731
|
}
|
|
491
732
|
|
|
733
|
+
// A panel rendered already-open never passes through openDropdown(), so its
|
|
734
|
+
// fit has to be taken here too — otherwise a story or a server-rendered bar
|
|
735
|
+
// keeps the fallback width and #549 survives in exactly the place a
|
|
736
|
+
// default-open menu makes most visible.
|
|
737
|
+
if (panel && !dd.__ddPanel && dd.classList.contains('open')) {
|
|
738
|
+
ddFitFilterPanel(dd, panel);
|
|
739
|
+
ddWatchRow(dd, panel);
|
|
740
|
+
}
|
|
741
|
+
|
|
492
742
|
trigger.addEventListener('click', (e) => {
|
|
493
743
|
e.stopPropagation();
|
|
494
744
|
if (dd.classList.contains('open')) closeDropdown(dd);
|
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
import { button, esc } from './index.js';
|
|
2
2
|
import { dropdown, wireDropdown } from './dropdown.js';
|
|
3
|
-
import { filterChipText, filterChipName, filterChipUnset, focusNextStop } from '../logic/filter-bar.js';
|
|
3
|
+
import { filterChipText, filterChipName, filterChipUnset, filterChipItems, focusNextStop } from '../logic/filter-bar.js';
|
|
4
4
|
|
|
5
5
|
export function filterBar({ filters = [], label = 'Filters', clearLabel = 'Clear all filters', disabled = false, busy = false } = {}) {
|
|
6
6
|
return `<fieldset class="ui-filter-bar" data-filter-bar${disabled || busy ? ' disabled' : ''}${busy ? ' aria-busy="true"' : ''}>`
|
|
7
7
|
+ `<legend class="ui-filter-bar__legend">${esc(label)}</legend>`
|
|
8
8
|
+ filters.map(filter => `<fieldset class="ui-filter-bar__chip" data-filter-id="${esc(filter.id)}"${filter.disabled ? ' disabled' : ''}>`
|
|
9
9
|
+ `<legend class="ui-filter-bar__legend">${esc(filter.label)}</legend>`
|
|
10
|
-
+ dropdown({ items: filter
|
|
10
|
+
+ dropdown({ items: filterChipItems(filter), variant: 'select',
|
|
11
11
|
// The chip prints one line; the field's name reaches a reader through the
|
|
12
12
|
// trigger's name and the chip's own legend. why: docs/specification.md#dense-financial-tables
|
|
13
13
|
triggerContent: `<span class="${filterChipUnset(filter) ? 'ui-dropdown__value is-placeholder' : 'ui-dropdown__value'}">${esc(filterChipText(filter))}</span>`,
|
package/src/index.d.ts
CHANGED
|
@@ -12,6 +12,12 @@ export declare function filterChipName(filter?: { label?: string; value?: string
|
|
|
12
12
|
export declare function filterChipUnset(filter?: { value?: string }): boolean;
|
|
13
13
|
export declare function nextFocusStop(host: Element | null | undefined): HTMLElement | null;
|
|
14
14
|
export declare function focusNextStop(host: Element | null | undefined): HTMLElement | null;
|
|
15
|
+
/** The chip's items with the chip's own `value` marked `selected`, so the line the chip
|
|
16
|
+
* prints and the row its menu marks cannot disagree. A row is matched by `value`, or by
|
|
17
|
+
* `label` where it has none. Every row's flag is written, so a chip that is unset or
|
|
18
|
+
* whose value is in no row marks nothing rather than leaving a `selected` the caller
|
|
19
|
+
* set standing. The caller's array and objects are not written to. */
|
|
20
|
+
export declare function filterChipItems<T>(filter?: { value?: string; items?: T[] }): T[];
|
|
15
21
|
export declare function segmentedNextIndex(key: string, index: number, length: number): number | null;
|
|
16
22
|
export declare function icon(name: string, className?: string): string;
|
|
17
23
|
export declare function snippet(opts?: Record<string, unknown>): string;
|
|
@@ -28,6 +34,18 @@ export declare function wireDropdown(root?: Document | Element): void;
|
|
|
28
34
|
/** The kit's own match, asked rather than re-implemented by <Dropdown>. */
|
|
29
35
|
export declare function dropdownMatch(label: unknown, query: unknown): boolean;
|
|
30
36
|
export declare function dropdownFiltering(query: unknown): boolean;
|
|
37
|
+
/** The kit's menu floor, the width `.ui-dropdown__panel` writes. */
|
|
38
|
+
export declare const DD_MENU_FLOOR: number;
|
|
39
|
+
/** Where a filter chip's open menu can sit: the room from the edge it is
|
|
40
|
+
* anchored at to the far side of its row once slid, how far it had to slide,
|
|
41
|
+
* the width it reached for, and whether it is anchored at its inline end.
|
|
42
|
+
* `null` unless the dropdown is inside a `.ui-filter-bar__chip`: the slide is a
|
|
43
|
+
* chip's offset along its row, so a panel anchored to the row itself is not this
|
|
44
|
+
* function's subject and sizes itself. why: docs/library.md */
|
|
45
|
+
export declare function filterPanelFit(
|
|
46
|
+
dd: Element | null | undefined,
|
|
47
|
+
floor?: number,
|
|
48
|
+
): { room: number; shift: number; floor: number; end: boolean } | null;
|
|
31
49
|
export declare function backLink(opts?: Record<string, unknown>): string;
|
|
32
50
|
export declare function statBand(opts?: Record<string, unknown>): string;
|
|
33
51
|
export declare function commandPalette(opts?: Record<string, unknown>): string;
|
package/src/index.js
CHANGED
|
@@ -30,7 +30,7 @@ export * from './components/filter-bar.js';
|
|
|
30
30
|
export { dropdownMatch, dropdownFiltering } from './logic/dropdown.js';
|
|
31
31
|
export { SCORE, rankGroups, rankCommands, scoreCommand, paletteHotkey } from './logic/command-palette.js';
|
|
32
32
|
export { segmentedNextIndex } from './logic/segmented.js';
|
|
33
|
-
export { filterChipText, filterChipName, filterChipUnset, nextFocusStop, focusNextStop } from './logic/filter-bar.js';
|
|
33
|
+
export { filterChipText, filterChipName, filterChipUnset, filterChipItems, nextFocusStop, focusNextStop } from './logic/filter-bar.js';
|
|
34
34
|
export { PAGE_SIZES, DEFAULT_PAGE_SIZE } from './logic/pagination.js';
|
|
35
35
|
export { calloutIcons } from './logic/callout.js';
|
|
36
36
|
export { formatNumericValue, formatDeltaValue } from './logic/table-values.js';
|
package/src/logic/filter-bar.js
CHANGED
|
@@ -103,3 +103,23 @@ export function focusNextStop(host) {
|
|
|
103
103
|
}
|
|
104
104
|
return null;
|
|
105
105
|
}
|
|
106
|
+
|
|
107
|
+
/** The chip's menu with the chip's own value marked, so the line the chip prints
|
|
108
|
+
* and the row the menu marks cannot disagree. A row is identified the way the
|
|
109
|
+
* dropdown identifies it when it reports a pick: by `value`, or by `label` where
|
|
110
|
+
* it carries none.
|
|
111
|
+
*
|
|
112
|
+
* Every row's flag is written, not only the chosen one's, so a chip with nothing
|
|
113
|
+
* chosen or a value in no row marks no row. Handing those items back as the
|
|
114
|
+
* consumer wrote them left whatever they had marked standing — a menu checking
|
|
115
|
+
* `All`, `aria-selected="true"` on it, under a chip printing `Asia`. The
|
|
116
|
+
* consumer's own array and objects are not written to. #550
|
|
117
|
+
* why: docs/specification.md#a-filter-row-holds-its-panels */
|
|
118
|
+
export function filterChipItems(filter = {}) {
|
|
119
|
+
const items = filter.items || [];
|
|
120
|
+
const chosen = text(filter.value);
|
|
121
|
+
const row = (it) => !!it && it !== '---' && !it.separator;
|
|
122
|
+
const key = (it) => text(it.value != null ? it.value : it.label);
|
|
123
|
+
const inForce = !filterChipUnset(filter) && items.some((it) => row(it) && key(it) === chosen);
|
|
124
|
+
return items.map((it) => (row(it) ? { ...it, selected: inForce && key(it) === chosen } : it));
|
|
125
|
+
}
|
package/src/motion.d.ts
CHANGED
|
@@ -4,3 +4,5 @@ export declare function initReveal(root?: Document | Element): IntersectionObser
|
|
|
4
4
|
export declare const ENTRANCE_FALLBACK_MS: number;
|
|
5
5
|
export declare function playEntrance(element: Element | null, className?: string): void;
|
|
6
6
|
export declare function replay(element: HTMLElement | null): void;
|
|
7
|
+
/** How long the stylesheet says this element's transition lasts, in ms; 0 off-DOM. */
|
|
8
|
+
export declare function transitionMs(element: Element | null | undefined): number;
|
package/src/motion.js
CHANGED
|
@@ -100,6 +100,33 @@ export function playEntrance(el, className = 'is-entering') {
|
|
|
100
100
|
entering.set(el, () => done());
|
|
101
101
|
}
|
|
102
102
|
|
|
103
|
+
/**
|
|
104
|
+
* How long the stylesheet says this element's transition lasts — the longest
|
|
105
|
+
* duration plus its own delay, in ms, and 0 off-DOM or for no element at all.
|
|
106
|
+
*
|
|
107
|
+
* What a backstop timer beside a `transitionend` listener is sized from, so the
|
|
108
|
+
* duration stays the sheet's rather than a copy of --dur-med. The event does not
|
|
109
|
+
* come for a transition that did not run, and jsdom never fires it at all.
|
|
110
|
+
*/
|
|
111
|
+
export function transitionMs(el) {
|
|
112
|
+
// getComputedStyle throws a TypeError for a non-element, and the view below
|
|
113
|
+
// falls back to the global window, so a missing ref took its caller down.
|
|
114
|
+
if (el?.nodeType !== 1) return 0;
|
|
115
|
+
const view = el.ownerDocument?.defaultView
|
|
116
|
+
|| (typeof window === 'undefined' ? null : window);
|
|
117
|
+
const cs = view?.getComputedStyle?.(el);
|
|
118
|
+
if (!cs) return 0;
|
|
119
|
+
// A delay list shorter than the duration list repeats, as the spec says.
|
|
120
|
+
const times = (list) => String(list || '').split(',').map((t) => {
|
|
121
|
+
const n = Number.parseFloat(t);
|
|
122
|
+
if (!Number.isFinite(n)) return 0;
|
|
123
|
+
return t.trim().endsWith('ms') ? n : n * 1000;
|
|
124
|
+
});
|
|
125
|
+
const durations = times(cs.transitionDuration);
|
|
126
|
+
const delays = times(cs.transitionDelay);
|
|
127
|
+
return Math.max(0, ...durations.map((d, i) => d + delays[i % delays.length]));
|
|
128
|
+
}
|
|
129
|
+
|
|
103
130
|
/** Restart the CSS animation on an element (for a "replay" control). */
|
|
104
131
|
export function replay(el) {
|
|
105
132
|
if (!el || !el.style) return;
|
|
@@ -12,16 +12,9 @@
|
|
|
12
12
|
take. `:enabled` leaves a disabled chip its own ink. why: guidelines/labels-and-titles.md */
|
|
13
13
|
.ui-filter-bar button:enabled .ui-dropdown__value.is-placeholder { /* muted-ink: placeholder — a filter chip with nothing chosen */ color: var(--muted); }
|
|
14
14
|
|
|
15
|
-
/*
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
screen — shut as well as open, a `visibility: hidden` box still being laid out.
|
|
19
|
-
The percentage resolves against .ui-dropdown, the panel's containing block,
|
|
20
|
-
which is the trigger's width because the trigger is its one in-flow child.
|
|
21
|
-
`overflow-wrap` keeps that a bound rather than a clip: a panel is
|
|
22
|
-
`overflow: visible`, so a value with no break opportunity would run out of it
|
|
23
|
-
and off the page. It inherits, reaching an option's label and description.
|
|
24
|
-
why: docs/specification.md#a-filter-row-holds-its-panels */
|
|
15
|
+
/* Shut, a panel is the trigger's width, never the 240px floor a standalone
|
|
16
|
+
dropdown gets: it is laid out while hidden, so a wider one widened the page
|
|
17
|
+
(#467). why: docs/specification.md#a-filter-row-holds-its-panels */
|
|
25
18
|
.ui-filter-bar .ui-dropdown__panel { min-width: 100%; max-width: 100%; overflow-wrap: anywhere; }
|
|
26
19
|
/* The clear action takes the kit's bordered skin minus its fill: words belong on
|
|
27
20
|
the page, not on a grey block, in any state — Artur, round r28. Rest stays at
|
|
@@ -39,3 +32,55 @@
|
|
|
39
32
|
measures on every ground. The edge stays the kit's --disabled-border. */
|
|
40
33
|
.ui-filter-bar [data-filter-clear] button:disabled,
|
|
41
34
|
.ui-filter-bar [data-filter-clear] button[aria-disabled="true"] { /* muted-ink: state — the clear action is off and its words sit on the page */ color: var(--disabled-ink-bare); }
|
|
35
|
+
|
|
36
|
+
/* Open, a chip's menu takes the kit's menu floor, measured and slid by
|
|
37
|
+
filterPanelFit(); `is-closing` holds that geometry through the fade. #549
|
|
38
|
+
why: docs/specification.md#a-filter-row-holds-its-panels */
|
|
39
|
+
.ui-filter-bar__chip .ui-dropdown.open .ui-dropdown__panel,
|
|
40
|
+
.ui-filter-bar__chip .ui-dropdown__panel.is-closing {
|
|
41
|
+
min-width: min(var(--ui-filter-panel-floor, 240px), var(--ui-filter-panel-room, 100%));
|
|
42
|
+
max-width: var(--ui-filter-panel-room, 100%);
|
|
43
|
+
margin-inline-start: calc(-1 * var(--ui-filter-panel-shift, 0px));
|
|
44
|
+
}
|
|
45
|
+
/* An end-aligned panel is pinned by its right edge, so the slide pushes it. */
|
|
46
|
+
.ui-filter-bar__chip .ui-dropdown.open .ui-dropdown__panel.is-end,
|
|
47
|
+
.ui-filter-bar__chip .ui-dropdown__panel.is-end.is-closing {
|
|
48
|
+
margin-inline-start: 0;
|
|
49
|
+
margin-inline-end: calc(-1 * var(--ui-filter-panel-shift, 0px));
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/* A 16px tick and an 11px gap do not fit the 48px trigger a chip printing `All`
|
|
53
|
+
gives its SHUT panel, and overflow-wrap cannot break a mark; the clip is what
|
|
54
|
+
shrinks the glyph and not just its box.
|
|
55
|
+
|
|
56
|
+
Shut is the whole of it. Written for every panel in a chip, this reached the
|
|
57
|
+
open menu too and left 9.8px of the check in a 240px menu — a chevron, not a
|
|
58
|
+
tick. Open the menu is at the floor with room for the mark, and `is-closing`
|
|
59
|
+
holds the open geometry through the fade, so it is open for this as well. #550
|
|
60
|
+
why: docs/specification.md#a-filter-row-holds-its-panels */
|
|
61
|
+
.ui-filter-bar__chip .ui-dropdown:not(.open) .ui-dropdown__panel:not(.is-closing) .ui-dropdown__tick {
|
|
62
|
+
flex-shrink: 1;
|
|
63
|
+
min-width: 0;
|
|
64
|
+
overflow: hidden;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/* The chosen value takes --glow-purple, the wash .ui-nav--tabs.is-pill gives the
|
|
68
|
+
tab you are on, and keeps the kit's 16px check. The wash is a 1.2:1 tint, so the
|
|
69
|
+
check is what identifies the state under WCAG 1.4.1; Apple, Material, Carbon,
|
|
70
|
+
Primer and Radix all tick a chosen menu row. #549
|
|
71
|
+
why: docs/specification.md#a-filter-row-holds-its-panels */
|
|
72
|
+
.ui-filter-bar .ui-dropdown__item.is-selected { background: var(--glow-purple); /* ring-gap: inherit — translucent wash retains the opaque containing gap. */ }
|
|
73
|
+
/* Kit-wide a chosen label steps down to --text, which left the row in force the
|
|
74
|
+
faintest in the panel. Under the wash it holds --strong. */
|
|
75
|
+
.ui-filter-bar .ui-dropdown__item.is-selected .ui-dropdown__label { color: var(--strong); }
|
|
76
|
+
/* The keyboard cursor and focus each set an opaque --surface, which would take the
|
|
77
|
+
wash off; restated here so the guarantee is one declaration a reader and
|
|
78
|
+
filter-selected-mark.test.js can both find, and each still reads by its own paint.
|
|
79
|
+
why: guidelines/accessibility-floor.md#measurable-pairs */
|
|
80
|
+
.ui-filter-bar .ui-dropdown__item.is-selected:focus-visible,
|
|
81
|
+
.ui-filter-bar .ui-dropdown__item.is-selected.is-active { background: var(--glow-purple); /* ring-gap: inherit — translucent wash retains the opaque containing gap. */ }
|
|
82
|
+
/* Hover's other paint is a 1px edge, 1.06:1 on the wash, which left the row in force
|
|
83
|
+
the one row a pointer got no answer from. Selection is a background highlight, so
|
|
84
|
+
hover deepens it: 9% more accent, the step .ui-table--zebra gives a hovered row.
|
|
85
|
+
why: docs/specification.md#a-filter-row-holds-its-panels */
|
|
86
|
+
.ui-filter-bar .ui-dropdown__item.is-selected:hover { background: color-mix(in srgb, var(--glow-purple), var(--accent) 9%); /* ring-gap: inherit — translucent wash retains the opaque containing gap. */ }
|
package/src/styles/topbar.css
CHANGED
|
@@ -191,7 +191,7 @@
|
|
|
191
191
|
src/styles/dropdown.css:176 `.ui-dropdown__item:hover`.
|
|
192
192
|
why: docs/specification.md#elevation */
|
|
193
193
|
.vopt:hover { outline: 1px solid var(--control-edge); outline-offset: -1px; background: var(--surface); /* ring-gap: inherit — control paint; its focus indicator uses the containing surface. */ }
|
|
194
|
-
/* The row is a keyboard stop: src/components/dropdown.js:
|
|
194
|
+
/* The row is a keyboard stop: src/components/dropdown.js:804
|
|
195
195
|
`items[next < 0 ? 0 : next]?.focus()` moves focus onto a `[data-dd-item]` with
|
|
196
196
|
the arrow keys, and this is one. It takes the ring on its own 10px radius, the
|
|
197
197
|
same way `.amenu a` above does. #482 */
|