@apliteni/apliteni-ui 0.37.0 → 0.38.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/README.md +4 -0
- package/package.json +1 -1
- package/react/dist/index.d.ts +64 -2
- package/react/dist/index.js +160 -47
- package/src/components/filter-bar.js +56 -0
- package/src/components/index.js +7 -7
- package/src/components/segmented.js +40 -0
- package/src/components/success.js +2 -2
- package/src/components/table-values.js +32 -0
- package/src/index.css +1 -0
- package/src/index.js +4 -0
- package/src/inline.js +3 -1
- package/src/styles/callout.css +2 -6
- package/src/styles/filter-bar.css +10 -0
- package/src/styles/segmented.css +7 -0
- package/src/styles/success.css +4 -2
- package/src/styles/table.css +45 -3
- package/src/tokens/tokens.css +5 -0
package/README.md
CHANGED
|
@@ -105,6 +105,10 @@ import { tokensCss, topbarCss, cssText } from '@apliteni/apliteni-ui/inline';
|
|
|
105
105
|
// …inline tokensCss + topbarCss into the <style> you serve.
|
|
106
106
|
```
|
|
107
107
|
|
|
108
|
+
`cssText` includes every sheet. When selecting individual sheets from `styles`,
|
|
109
|
+
`successPanel()` needs both `styles.callout` (panel layout) and `styles.success`
|
|
110
|
+
(shared glowing check and reduced-motion styles), after `tokensCss` and `baseCss`.
|
|
111
|
+
|
|
108
112
|
## React components (stateful surfaces)
|
|
109
113
|
|
|
110
114
|
`DataTable`, `Pagination`, `Modal`, `Button`, `Badge`, `Card` and `Icon` — same `.ui-*` classes,
|
package/package.json
CHANGED
package/react/dist/index.d.ts
CHANGED
|
@@ -162,6 +162,12 @@ type PagerProps = {
|
|
|
162
162
|
type DataTableProps<T> = {
|
|
163
163
|
columns: Column<T>[];
|
|
164
164
|
rows: T[];
|
|
165
|
+
dense?: boolean;
|
|
166
|
+
density?: 'default' | 'dense' | 'compact';
|
|
167
|
+
stickyHeader?: boolean;
|
|
168
|
+
pinnedIdentity?: boolean;
|
|
169
|
+
scrollLabel?: string;
|
|
170
|
+
empty?: ReactNode;
|
|
165
171
|
pageSize?: number;
|
|
166
172
|
pageSizes?: readonly number[] | null;
|
|
167
173
|
onPageSizeChange?: (size: number) => void;
|
|
@@ -185,7 +191,7 @@ type DataTableProps<T> = {
|
|
|
185
191
|
declare function sortTableRows<T>(rows: T[], sort: TableSort<T>): T[];
|
|
186
192
|
declare function DataTable<T extends {
|
|
187
193
|
name: string;
|
|
188
|
-
}>({ columns, rows, pageSize, pageSizes, onPageSizeChange, pager, pagerLabel, loading, selectable, selected, onToggle, onTogglePage, sort: controlledSort, onSortChange, page: controlledPage, onPageChange, total, hasMore, }: DataTableProps<T>): react.JSX.Element;
|
|
194
|
+
}>({ columns, rows, dense, density, stickyHeader, pinnedIdentity, scrollLabel, empty, pageSize, pageSizes, onPageSizeChange, pager, pagerLabel, loading, selectable, selected, onToggle, onTogglePage, sort: controlledSort, onSortChange, page: controlledPage, onPageChange, total, hasMore, }: DataTableProps<T>): react.JSX.Element;
|
|
189
195
|
|
|
190
196
|
type PaginationProps = {
|
|
191
197
|
page?: number;
|
|
@@ -363,7 +369,63 @@ type BackLinkProps<T extends ElementType = 'a'> = BackLinkOwnProps & {
|
|
|
363
369
|
*/
|
|
364
370
|
declare function BackLink<T extends ElementType = 'a'>({ href, label, as, className, ...rest }: BackLinkProps<T>): react.JSX.Element | null;
|
|
365
371
|
|
|
372
|
+
type NumericValueProps = {
|
|
373
|
+
value?: string | number | null;
|
|
374
|
+
unit?: string;
|
|
375
|
+
missing?: string;
|
|
376
|
+
};
|
|
377
|
+
type DeltaValueProps = {
|
|
378
|
+
value?: string | null;
|
|
379
|
+
tone?: 'success' | 'danger' | 'neutral';
|
|
380
|
+
basisId?: string;
|
|
381
|
+
missing?: string;
|
|
382
|
+
};
|
|
383
|
+
declare function NumericValue(props: NumericValueProps): react.JSX.Element;
|
|
384
|
+
declare function DeltaValue(props: DeltaValueProps): react.JSX.Element;
|
|
385
|
+
type RowIdentityProps = {
|
|
386
|
+
symbol: string;
|
|
387
|
+
name: string;
|
|
388
|
+
logo?: string;
|
|
389
|
+
href?: string;
|
|
390
|
+
};
|
|
391
|
+
declare function RowIdentity({ symbol, name, logo, href }: RowIdentityProps): react.JSX.Element;
|
|
392
|
+
|
|
393
|
+
type SegmentOption = {
|
|
394
|
+
label: string;
|
|
395
|
+
value: string;
|
|
396
|
+
disabled?: boolean;
|
|
397
|
+
};
|
|
398
|
+
type SegmentedProps = {
|
|
399
|
+
options: SegmentOption[];
|
|
400
|
+
value: string;
|
|
401
|
+
onChange: (value: string) => void;
|
|
402
|
+
label: string;
|
|
403
|
+
appearance?: 'pill' | 'underline';
|
|
404
|
+
disabled?: boolean;
|
|
405
|
+
};
|
|
406
|
+
declare function Segmented({ options, value, onChange, label, appearance, disabled }: SegmentedProps): react.JSX.Element;
|
|
407
|
+
|
|
408
|
+
type Filter = {
|
|
409
|
+
id: string;
|
|
410
|
+
label: string;
|
|
411
|
+
value: string;
|
|
412
|
+
items: DropdownEntry[];
|
|
413
|
+
disabled?: boolean;
|
|
414
|
+
open?: boolean;
|
|
415
|
+
};
|
|
416
|
+
type FilterBarProps = {
|
|
417
|
+
filters: Filter[];
|
|
418
|
+
label?: string;
|
|
419
|
+
clearLabel?: string;
|
|
420
|
+
disabled?: boolean;
|
|
421
|
+
busy?: boolean;
|
|
422
|
+
onRemove: (id: string) => void;
|
|
423
|
+
onClear: () => void;
|
|
424
|
+
onChange: (id: string, value: string | undefined) => void;
|
|
425
|
+
};
|
|
426
|
+
declare function FilterBar(props: FilterBarProps): react.JSX.Element;
|
|
427
|
+
|
|
366
428
|
declare const PAGE_SIZES: readonly number[];
|
|
367
429
|
declare const DEFAULT_PAGE_SIZE: number;
|
|
368
430
|
|
|
369
|
-
export { BackLink, type BackLinkOwnProps, type BackLinkProps, Badge, BusyRegion, type BusyRegionProps, Button, type ButtonProps, Card, type Column, type CommandGroup, type CommandItem, CommandPalette, type CommandPaletteProps, DEFAULT_PAGE_SIZE, DataTable, type DataTableProps, Denied, type DeniedProps, Drawer, type DrawerProps, Dropdown, type DropdownBadge, type DropdownEntry, type DropdownItem, type DropdownProps, type DropdownRowProps, type DropdownSearch, type DropdownSection, type DropdownSeparator, Icon, Modal, type ModalProps, PAGE_SIZES, Pagination, type PaginationProps, Skeleton, type SkeletonProps, SkeletonTable, type SkeletonTableProps, StatBand, type StatBandProps, type StatDelta, type StatFigure, type StatTone, type StatVariant, type TableSort, sortTableRows };
|
|
431
|
+
export { BackLink, type BackLinkOwnProps, type BackLinkProps, Badge, BusyRegion, type BusyRegionProps, Button, type ButtonProps, Card, type Column, type CommandGroup, type CommandItem, CommandPalette, type CommandPaletteProps, DEFAULT_PAGE_SIZE, DataTable, type DataTableProps, DeltaValue, type DeltaValueProps, Denied, type DeniedProps, Drawer, type DrawerProps, Dropdown, type DropdownBadge, type DropdownEntry, type DropdownItem, type DropdownProps, type DropdownRowProps, type DropdownSearch, type DropdownSection, type DropdownSeparator, type Filter, FilterBar, type FilterBarProps, Icon, Modal, type ModalProps, NumericValue, type NumericValueProps, PAGE_SIZES, Pagination, type PaginationProps, RowIdentity, type RowIdentityProps, type SegmentOption, Segmented, type SegmentedProps, Skeleton, type SkeletonProps, SkeletonTable, type SkeletonTableProps, StatBand, type StatBandProps, type StatDelta, type StatFigure, type StatTone, type StatVariant, type TableSort, sortTableRows };
|
package/react/dist/index.js
CHANGED
|
@@ -842,6 +842,12 @@ function sortTableRows(rows, sort) {
|
|
|
842
842
|
function DataTable({
|
|
843
843
|
columns,
|
|
844
844
|
rows,
|
|
845
|
+
dense = false,
|
|
846
|
+
density,
|
|
847
|
+
stickyHeader = false,
|
|
848
|
+
pinnedIdentity = false,
|
|
849
|
+
scrollLabel = "Table",
|
|
850
|
+
empty = "No rows",
|
|
845
851
|
pageSize,
|
|
846
852
|
pageSizes = null,
|
|
847
853
|
onPageSizeChange,
|
|
@@ -896,54 +902,66 @@ function DataTable({
|
|
|
896
902
|
const caret = (k) => sort.key === k ? sort.dir === 1 ? " \u25B2" : " \u25BC" : " \u2195";
|
|
897
903
|
const pageAllOn = selectable && slice.length > 0 && slice.every((r) => selected.has(r.name));
|
|
898
904
|
return /* @__PURE__ */ jsxs8(Fragment2, { children: [
|
|
899
|
-
/* @__PURE__ */
|
|
900
|
-
"
|
|
905
|
+
/* @__PURE__ */ jsx10(
|
|
906
|
+
"div",
|
|
901
907
|
{
|
|
902
|
-
className: "ui-table
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
908
|
+
className: stickyHeader || pinnedIdentity ? "ui-table-scroll" : void 0,
|
|
909
|
+
role: stickyHeader || pinnedIdentity ? "region" : void 0,
|
|
910
|
+
"aria-label": stickyHeader || pinnedIdentity ? scrollLabel : void 0,
|
|
911
|
+
tabIndex: stickyHeader || pinnedIdentity ? 0 : void 0,
|
|
912
|
+
children: /* @__PURE__ */ jsxs8(
|
|
913
|
+
"table",
|
|
914
|
+
{
|
|
915
|
+
className: ["ui-table ui-table--hover", (density === "dense" || !density && dense) && "ui-table--dense", density === "compact" && "ui-table--compact", stickyHeader && "ui-table--sticky", pinnedIdentity && "ui-table--pinned"].filter(Boolean).join(" "),
|
|
916
|
+
"aria-busy": loading || void 0,
|
|
917
|
+
children: [
|
|
918
|
+
/* @__PURE__ */ jsx10("thead", { children: /* @__PURE__ */ jsxs8("tr", { children: [
|
|
919
|
+
selectable ? /* @__PURE__ */ jsx10("th", { scope: "col", className: "ui-table__selection", children: /* @__PURE__ */ jsx10(
|
|
920
|
+
"input",
|
|
921
|
+
{
|
|
922
|
+
type: "checkbox",
|
|
923
|
+
checked: pageAllOn,
|
|
924
|
+
"aria-label": "Select all rows on this page",
|
|
925
|
+
onChange: () => onTogglePage(slice.map((r) => r.name))
|
|
926
|
+
}
|
|
927
|
+
) }) : null,
|
|
928
|
+
columns.map((c, columnIndex) => (
|
|
929
|
+
// The sort control is a real <button> inside the header cell. It used to be
|
|
930
|
+
// role="button" ON the <th>, which threw away the columnheader role and put
|
|
931
|
+
// aria-sort on a role that forbids it.
|
|
932
|
+
/* @__PURE__ */ jsx10(
|
|
933
|
+
"th",
|
|
934
|
+
{
|
|
935
|
+
scope: "col",
|
|
936
|
+
className: [pinnedIdentity && columnIndex === 0 && "ui-table__identity", c.num && "ui-table__num", c.sortable && "rx-sortable"].filter(Boolean).join(" "),
|
|
937
|
+
"aria-sort": sort.key === c.key ? sort.dir === 1 ? "ascending" : "descending" : c.sortable && sortIsKnown ? "none" : void 0,
|
|
938
|
+
children: c.sortable ? /* @__PURE__ */ jsxs8("button", { type: "button", className: "rx-sort", onClick: () => onSort(c.key), children: [
|
|
939
|
+
c.label,
|
|
940
|
+
/* @__PURE__ */ jsx10("span", { className: "rx-caret", "aria-hidden": "true", children: caret(c.key) })
|
|
941
|
+
] }) : c.label
|
|
942
|
+
},
|
|
943
|
+
c.key
|
|
944
|
+
)
|
|
945
|
+
))
|
|
946
|
+
] }) }),
|
|
947
|
+
/* @__PURE__ */ jsxs8("tbody", { children: [
|
|
948
|
+
!slice.length && /* @__PURE__ */ jsx10("tr", { children: /* @__PURE__ */ jsx10("td", { colSpan: columns.length + (selectable ? 1 : 0), children: loading ? "Loading rows\u2026" : empty }) }),
|
|
949
|
+
slice.map((r) => /* @__PURE__ */ jsxs8("tr", { children: [
|
|
950
|
+
selectable ? /* @__PURE__ */ jsx10("td", { className: "ui-table__selection", children: /* @__PURE__ */ jsx10(
|
|
951
|
+
"input",
|
|
952
|
+
{
|
|
953
|
+
type: "checkbox",
|
|
954
|
+
checked: selected.has(r.name),
|
|
955
|
+
"aria-label": `Select ${r.name}`,
|
|
956
|
+
onChange: () => onToggle(r.name)
|
|
957
|
+
}
|
|
958
|
+
) }) : null,
|
|
959
|
+
columns.map((c, columnIndex) => /* @__PURE__ */ jsx10("td", { className: [c.num && "ui-table__num", pinnedIdentity && columnIndex === 0 && "ui-table__identity"].filter(Boolean).join(" "), children: c.render ? c.render(r) : String(r[c.key]) }, c.key))
|
|
960
|
+
] }, r.name))
|
|
961
|
+
] })
|
|
962
|
+
]
|
|
963
|
+
}
|
|
964
|
+
)
|
|
947
965
|
}
|
|
948
966
|
),
|
|
949
967
|
pager ? /* @__PURE__ */ jsx10(
|
|
@@ -1470,6 +1488,96 @@ function BackLink({
|
|
|
1470
1488
|
);
|
|
1471
1489
|
}
|
|
1472
1490
|
|
|
1491
|
+
// src/TableValues.tsx
|
|
1492
|
+
import { numericValue, deltaValue } from "@apliteni/apliteni-ui";
|
|
1493
|
+
import { jsx as jsx14, jsxs as jsxs12 } from "react/jsx-runtime";
|
|
1494
|
+
function NumericValue(props) {
|
|
1495
|
+
return /* @__PURE__ */ jsx14("span", { dangerouslySetInnerHTML: { __html: numericValue(props) } });
|
|
1496
|
+
}
|
|
1497
|
+
function DeltaValue(props) {
|
|
1498
|
+
return /* @__PURE__ */ jsx14("span", { dangerouslySetInnerHTML: { __html: deltaValue(props) } });
|
|
1499
|
+
}
|
|
1500
|
+
function RowIdentity({ symbol, name, logo, href }) {
|
|
1501
|
+
const Tag = href ? "a" : "span";
|
|
1502
|
+
return /* @__PURE__ */ jsxs12(Tag, { className: "ui-identity", href, children: [
|
|
1503
|
+
/* @__PURE__ */ jsxs12("span", { className: "ui-identity__logo", "aria-hidden": "true", children: [
|
|
1504
|
+
/* @__PURE__ */ jsx14("span", { children: symbol.slice(0, 1) }),
|
|
1505
|
+
logo && /* @__PURE__ */ jsx14("img", { src: logo, alt: "", onLoad: (e) => {
|
|
1506
|
+
e.currentTarget.hidden = false;
|
|
1507
|
+
}, onError: (e) => {
|
|
1508
|
+
e.currentTarget.hidden = true;
|
|
1509
|
+
} })
|
|
1510
|
+
] }),
|
|
1511
|
+
/* @__PURE__ */ jsx14("span", { className: "ui-identity__symbol", children: symbol }),
|
|
1512
|
+
/* @__PURE__ */ jsx14("span", { className: "ui-identity__name", children: name })
|
|
1513
|
+
] });
|
|
1514
|
+
}
|
|
1515
|
+
|
|
1516
|
+
// src/Segmented.tsx
|
|
1517
|
+
import { segmentedNextIndex } from "@apliteni/apliteni-ui";
|
|
1518
|
+
import { jsx as jsx15 } from "react/jsx-runtime";
|
|
1519
|
+
function Segmented({ options, value, onChange, label, appearance = "pill", disabled = false }) {
|
|
1520
|
+
const enabled = options.filter((o) => !o.disabled);
|
|
1521
|
+
const tabValue = enabled.some((o) => o.value === value) ? value : enabled[0]?.value;
|
|
1522
|
+
return /* @__PURE__ */ jsx15("div", { className: `ui-seg${appearance === "underline" ? " ui-seg--underline" : ""}`, role: "toolbar", "aria-label": label, children: options.map((option) => /* @__PURE__ */ jsx15(
|
|
1523
|
+
"button",
|
|
1524
|
+
{
|
|
1525
|
+
type: "button",
|
|
1526
|
+
"data-value": option.value,
|
|
1527
|
+
disabled: disabled || option.disabled,
|
|
1528
|
+
className: value === option.value ? "is-active" : void 0,
|
|
1529
|
+
"aria-pressed": value === option.value,
|
|
1530
|
+
tabIndex: option.value === tabValue ? 0 : -1,
|
|
1531
|
+
onClick: () => onChange(option.value),
|
|
1532
|
+
onKeyDown: (e) => {
|
|
1533
|
+
const next = segmentedNextIndex(e.key, enabled.indexOf(option), enabled.length);
|
|
1534
|
+
if (next == null) return;
|
|
1535
|
+
e.preventDefault();
|
|
1536
|
+
const buttons = [...e.currentTarget.parentElement.querySelectorAll("button:not(:disabled)")];
|
|
1537
|
+
buttons[next]?.focus();
|
|
1538
|
+
onChange(enabled[next].value);
|
|
1539
|
+
},
|
|
1540
|
+
children: option.label
|
|
1541
|
+
},
|
|
1542
|
+
option.value
|
|
1543
|
+
)) });
|
|
1544
|
+
}
|
|
1545
|
+
|
|
1546
|
+
// src/FilterBar.tsx
|
|
1547
|
+
import { useLayoutEffect as useLayoutEffect2, useRef as useRef7 } from "react";
|
|
1548
|
+
import { filterBar, initFilterBar } from "@apliteni/apliteni-ui";
|
|
1549
|
+
import { jsx as jsx16 } from "react/jsx-runtime";
|
|
1550
|
+
function FilterBar(props) {
|
|
1551
|
+
const host = useRef7(null);
|
|
1552
|
+
const latest = useRef7(props);
|
|
1553
|
+
latest.current = props;
|
|
1554
|
+
const controller = useRef7(null);
|
|
1555
|
+
const initial = useRef7({ __html: filterBar(props) });
|
|
1556
|
+
useLayoutEffect2(() => {
|
|
1557
|
+
const el = host.current;
|
|
1558
|
+
controller.current = initFilterBar(el, latest.current);
|
|
1559
|
+
const remove = (e) => latest.current.onRemove(e.detail.id);
|
|
1560
|
+
const clear = () => latest.current.onClear();
|
|
1561
|
+
const change = (e) => {
|
|
1562
|
+
const { id, value } = e.detail;
|
|
1563
|
+
latest.current.onChange(id, value);
|
|
1564
|
+
};
|
|
1565
|
+
el.addEventListener("ui-filter-remove", remove);
|
|
1566
|
+
el.addEventListener("ui-filter-clear", clear);
|
|
1567
|
+
el.addEventListener("ui-filter-change", change);
|
|
1568
|
+
return () => {
|
|
1569
|
+
controller.current?.destroy();
|
|
1570
|
+
el.removeEventListener("ui-filter-remove", remove);
|
|
1571
|
+
el.removeEventListener("ui-filter-clear", clear);
|
|
1572
|
+
el.removeEventListener("ui-filter-change", change);
|
|
1573
|
+
};
|
|
1574
|
+
}, []);
|
|
1575
|
+
useLayoutEffect2(() => {
|
|
1576
|
+
controller.current?.update(props);
|
|
1577
|
+
}, [props.filters, props.label, props.clearLabel, props.disabled, props.busy]);
|
|
1578
|
+
return /* @__PURE__ */ jsx16("div", { ref: host, dangerouslySetInnerHTML: initial.current });
|
|
1579
|
+
}
|
|
1580
|
+
|
|
1473
1581
|
// src/index.ts
|
|
1474
1582
|
var PAGE_SIZES = KIT_PAGE_SIZES;
|
|
1475
1583
|
var DEFAULT_PAGE_SIZE3 = KIT_DEFAULT_PAGE_SIZE;
|
|
@@ -1482,13 +1590,18 @@ export {
|
|
|
1482
1590
|
CommandPalette,
|
|
1483
1591
|
DEFAULT_PAGE_SIZE3 as DEFAULT_PAGE_SIZE,
|
|
1484
1592
|
DataTable,
|
|
1593
|
+
DeltaValue,
|
|
1485
1594
|
Denied,
|
|
1486
1595
|
Drawer,
|
|
1487
1596
|
Dropdown,
|
|
1597
|
+
FilterBar,
|
|
1488
1598
|
Icon,
|
|
1489
1599
|
Modal,
|
|
1600
|
+
NumericValue,
|
|
1490
1601
|
PAGE_SIZES,
|
|
1491
1602
|
Pagination,
|
|
1603
|
+
RowIdentity,
|
|
1604
|
+
Segmented,
|
|
1492
1605
|
Skeleton,
|
|
1493
1606
|
SkeletonTable,
|
|
1494
1607
|
StatBand,
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { button, esc } from './index.js';
|
|
2
|
+
import { dropdown, wireDropdown } from './dropdown.js';
|
|
3
|
+
|
|
4
|
+
export function filterBar({ filters = [], label = 'Filters', clearLabel = 'Clear all filters', disabled = false, busy = false } = {}) {
|
|
5
|
+
return `<fieldset class="ui-filter-bar" data-filter-bar${disabled || busy ? ' disabled' : ''}${busy ? ' aria-busy="true"' : ''}>`
|
|
6
|
+
+ `<legend class="ui-filter-bar__legend">${esc(label)}</legend>`
|
|
7
|
+
+ filters.map(filter => `<fieldset class="ui-filter-bar__chip" data-filter-id="${esc(filter.id)}"${filter.disabled ? ' disabled' : ''}>`
|
|
8
|
+
+ `<legend class="ui-filter-bar__legend">${esc(filter.label)}</legend>`
|
|
9
|
+
+ dropdown({ label: filter.label, value: filter.value, items: filter.items || [], variant: 'select', ariaLabel: `${filter.label}: ${filter.value}`, open: !!filter.open && !disabled && !busy && !filter.disabled })
|
|
10
|
+
+ `<button type="button" class="ui-filter-bar__remove" data-filter-remove aria-label="${esc(`Remove ${filter.label} filter`)}">×</button></fieldset>`).join('')
|
|
11
|
+
+ `<span data-filter-clear>${button({ label: clearLabel, size: 'sm', variant: 'ghost', disabled: !filters.length })}</span></fieldset>`;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
// The host stays mounted; update() restores the action's focus after controlled removal.
|
|
15
|
+
export function initFilterBar(host, options = {}) {
|
|
16
|
+
let current = options;
|
|
17
|
+
const focusKey = () => {
|
|
18
|
+
const active = host.ownerDocument.activeElement;
|
|
19
|
+
if (!host.contains(active)) return null;
|
|
20
|
+
const chip = active.closest('[data-filter-id]');
|
|
21
|
+
return { id: chip?.dataset.filterId, remove: active.hasAttribute('data-filter-remove'),
|
|
22
|
+
index: [...host.querySelectorAll('[data-filter-id]')].indexOf(chip) };
|
|
23
|
+
};
|
|
24
|
+
const wire = () => wireDropdown(host);
|
|
25
|
+
const update = next => {
|
|
26
|
+
const focus = focusKey();
|
|
27
|
+
current = next;
|
|
28
|
+
host.innerHTML = filterBar(next); wire();
|
|
29
|
+
if (!focus) return;
|
|
30
|
+
const chips = [...host.querySelectorAll('[data-filter-id]')].filter(c => !c.disabled);
|
|
31
|
+
const same = chips.find(c => c.dataset.filterId === focus.id);
|
|
32
|
+
const target = same || chips[Math.min(Math.max(focus.index, 0), chips.length - 1)];
|
|
33
|
+
const control = target?.querySelector(same && focus.remove ? '[data-filter-remove]' : '[data-dropdown-trigger]')
|
|
34
|
+
|| host.querySelector('[data-filter-clear] button');
|
|
35
|
+
if (control && !control.disabled && !control.closest('fieldset:disabled')) control.focus();
|
|
36
|
+
else { const bar = host.querySelector('[data-filter-bar]'); bar.tabIndex = -1; bar.focus(); }
|
|
37
|
+
};
|
|
38
|
+
const emit = (type, detail) => host.dispatchEvent(new host.ownerDocument.defaultView.CustomEvent(type, { bubbles: true, detail }));
|
|
39
|
+
const click = e => {
|
|
40
|
+
if (e.target.closest('fieldset:disabled')) return;
|
|
41
|
+
const chip = e.target.closest('[data-filter-id]');
|
|
42
|
+
if (e.target.closest('[data-filter-remove]')) emit('ui-filter-remove', { id: chip.dataset.filterId });
|
|
43
|
+
else if (e.target.closest('[data-filter-clear] button')) emit('ui-filter-clear', {});
|
|
44
|
+
else if (e.target.closest('[data-dd-item]')) {
|
|
45
|
+
const item = e.target.closest('[data-dd-item]');
|
|
46
|
+
if (item.getAttribute('aria-disabled') !== 'true') {
|
|
47
|
+
const id = chip?.dataset.filterId, value = item.dataset.value;
|
|
48
|
+
// Dropdown completes its own close/focus before the consumer replaces the markup.
|
|
49
|
+
queueMicrotask(() => emit('ui-filter-change', { id, value }));
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
};
|
|
53
|
+
if (!host.querySelector('[data-filter-bar]')) host.innerHTML = filterBar(current);
|
|
54
|
+
host.addEventListener('click', click, true); wire();
|
|
55
|
+
return { update, destroy: () => host.removeEventListener('click', click, true) };
|
|
56
|
+
}
|
package/src/components/index.js
CHANGED
|
@@ -2,10 +2,10 @@
|
|
|
2
2
|
// viz/ server-render idiom so the portal can adopt them with no framework.
|
|
3
3
|
import { icon } from '../assets/icons.js';
|
|
4
4
|
import { illo } from '../assets/illustrations.js';
|
|
5
|
+
import { successCheck } from './success.js';
|
|
5
6
|
const HTML_ENTITIES = { '&': '&', '<': '<', '>': '>', '"': '"' };
|
|
6
7
|
const cx = (...a) => a.filter(Boolean).join(' ');
|
|
7
8
|
export const esc = (s) => String(s == null ? '' : s).replace(/[&<>"]/g, (c) => HTML_ENTITIES[c]);
|
|
8
|
-
|
|
9
9
|
// ---- Button --------------------------------------------------------------
|
|
10
10
|
// `iconSvg` is a raw leading-icon SVG string (trusted markup, not escaped) for
|
|
11
11
|
// branded glyphs the kit's icon set doesn't own — e.g. a Google "G". It takes
|
|
@@ -80,16 +80,16 @@ export function card({ title, sub, body = '', variant, pad, icon: ic, level = 2
|
|
|
80
80
|
// `ariaLabel` names the strip. `name` seeds data-seg and is deliberately
|
|
81
81
|
// not an accessible name — it is an identifier, not prose, defaulting to
|
|
82
82
|
// "Options" so a strip is never left unlabelled.
|
|
83
|
-
export function segmented({ options = [], active = 0, size, block, name = 'seg', ariaLabel = 'Options' } = {}) {
|
|
84
|
-
const cls = cx('ui-seg', size && `ui-seg--${size}`, block && 'ui-seg--block');
|
|
83
|
+
export function segmented({ options = [], active = 0, size, block, name = 'seg', ariaLabel = 'Options', appearance, disabled = false } = {}) {
|
|
84
|
+
const cls = cx('ui-seg', appearance === 'underline' && 'ui-seg--underline', size && `ui-seg--${size}`, block && 'ui-seg--block');
|
|
85
85
|
// Exactly one option holds the Tab stop. If `active` points nowhere (nothing
|
|
86
86
|
// selected yet) the first option holds it, so the strip is never unreachable.
|
|
87
|
-
const rove = active >= 0 && active < options.length ? active :
|
|
87
|
+
const rove = active >= 0 && active < options.length && !options[active]?.disabled ? active : options.findIndex(o => !o.disabled);
|
|
88
88
|
const btns = options.map((o, i) => {
|
|
89
89
|
const label = typeof o === 'string' ? o : o.label;
|
|
90
90
|
const val = typeof o === 'string' ? o : (o.value ?? o.label);
|
|
91
91
|
const on = i === active;
|
|
92
|
-
return `<button type="button" aria-pressed="${on}" tabindex="${i === rove ? '0' : '-1'}"`
|
|
92
|
+
return `<button type="button"${disabled || o.disabled ? ' disabled' : ''} aria-pressed="${on}" tabindex="${i === rove ? '0' : '-1'}"`
|
|
93
93
|
+ ` data-value="${esc(val)}"${on ? ' class="is-active"' : ''}>${esc(label)}</button>`;
|
|
94
94
|
}).join('');
|
|
95
95
|
return `<div class="${cls}" role="toolbar" aria-label="${esc(ariaLabel)}" data-seg="${name}">${btns}</div>`;
|
|
@@ -261,9 +261,9 @@ export function toast({
|
|
|
261
261
|
// The kit also publishes success() from components/success.js, which is the same
|
|
262
262
|
// idea at page size: layouts, a backdrop, follow-up buttons and an auto-redirect
|
|
263
263
|
// countdown. Pick by how much of the screen the confirmation owns. The two share
|
|
264
|
-
//
|
|
264
|
+
// the glowing check; their layout and content remain independent.
|
|
265
265
|
export function successPanel({ title = 'Done', sub = '' } = {}) {
|
|
266
|
-
return `<div class="ui-success"><div class="ui-success__check">${
|
|
266
|
+
return `<div class="ui-success"><div class="ui-success__check">${successCheck()}</div><div class="ui-success__title">${esc(title)}</div>${sub ? `<div class="ui-success__sub">${esc(sub)}</div>` : ''}</div>`;
|
|
267
267
|
}
|
|
268
268
|
|
|
269
269
|
// ---- Empty state ---------------------------------------------------------
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
export function segmentedNextIndex(key, index, length) {
|
|
2
|
+
if (!length) return null;
|
|
3
|
+
if (key === 'ArrowRight' || key === 'ArrowDown') return (index + 1) % length;
|
|
4
|
+
if (key === 'ArrowLeft' || key === 'ArrowUp') return (index - 1 + length) % length;
|
|
5
|
+
if (key === 'Home') return 0;
|
|
6
|
+
if (key === 'End') return length - 1;
|
|
7
|
+
return null;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function initSegmented(root = document) {
|
|
11
|
+
const cleanups = [];
|
|
12
|
+
root.querySelectorAll('[data-seg]').forEach(group => {
|
|
13
|
+
if (group.__segWired) return;
|
|
14
|
+
group.__segWired = true;
|
|
15
|
+
const buttons = () => [...group.querySelectorAll('button')].filter(b => !b.disabled);
|
|
16
|
+
const pick = button => {
|
|
17
|
+
group.querySelectorAll('button').forEach(b => {
|
|
18
|
+
const active = b === button;
|
|
19
|
+
b.setAttribute('aria-pressed', String(active));
|
|
20
|
+
b.classList.toggle('is-active', active);
|
|
21
|
+
b.tabIndex = active ? 0 : -1;
|
|
22
|
+
});
|
|
23
|
+
group.dispatchEvent(new group.ownerDocument.defaultView.CustomEvent('ui-segment-change', {
|
|
24
|
+
bubbles: true, detail: { value: button.dataset.value },
|
|
25
|
+
}));
|
|
26
|
+
};
|
|
27
|
+
const click = e => { const b = e.target.closest('button'); if (buttons().includes(b)) pick(b); };
|
|
28
|
+
const keydown = e => {
|
|
29
|
+
const list = buttons();
|
|
30
|
+
const index = list.indexOf(e.target);
|
|
31
|
+
if (index < 0) return;
|
|
32
|
+
const next = segmentedNextIndex(e.key, index, list.length);
|
|
33
|
+
if (next == null) return;
|
|
34
|
+
e.preventDefault(); list[next].focus(); pick(list[next]);
|
|
35
|
+
};
|
|
36
|
+
group.addEventListener('click', click); group.addEventListener('keydown', keydown);
|
|
37
|
+
cleanups.push(() => { group.removeEventListener('click', click); group.removeEventListener('keydown', keydown); delete group.__segWired; });
|
|
38
|
+
});
|
|
39
|
+
return () => cleanups.forEach(fn => fn());
|
|
40
|
+
}
|
|
@@ -60,8 +60,8 @@ function countdownEl({ seconds = 5, label = 'Redirecting' } = {}) {
|
|
|
60
60
|
// For a confirmation that stays inside the page the user is already on, the kit
|
|
61
61
|
// publishes successPanel() from components/index.js — a check, a title and one
|
|
62
62
|
// line of sub, and nothing to configure. Pick by how much of the screen the
|
|
63
|
-
// confirmation owns. The two share
|
|
64
|
-
//
|
|
63
|
+
// confirmation owns. The two share the glowing check, while their layout
|
|
64
|
+
// and content remain independent.
|
|
65
65
|
export function success({
|
|
66
66
|
layout = 'hero', // 'hero' | 'split' | 'compact'
|
|
67
67
|
level, // heading level of the title; see the note below
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { esc } from './index.js';
|
|
2
|
+
|
|
3
|
+
export function numericValue({ value, unit = '', missing = 'Not available' } = {}) {
|
|
4
|
+
if (value == null || value === '') return `<span class="ui-value" aria-label="${esc(missing)}">—</span>`;
|
|
5
|
+
return `<span class="ui-value">${esc(value)}${unit ? `<span class="ui-value__unit">${esc(unit)}</span>` : ''}</span>`;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export function deltaValue({ value, tone = 'neutral', basisId, missing = 'No earlier figure' } = {}) {
|
|
9
|
+
const present = value != null && value !== '';
|
|
10
|
+
const judged = present && ['success', 'danger'].includes(tone) && !/^[+−-]?0(?:[.,]0+)?%?$/.test(String(value).replace(/\s/g, ''));
|
|
11
|
+
return `<span class="ui-delta${judged ? ` ui-delta--${tone}` : ''}"${basisId ? ` aria-describedby="${esc(basisId)}"` : ''}>${esc(present ? value : missing)}</span>`;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function rowIdentity({ symbol = '', name = '', logo, href } = {}) {
|
|
15
|
+
const tag = href ? 'a' : 'span';
|
|
16
|
+
return `<${tag} class="ui-identity"${href ? ` href="${esc(href)}"` : ''}>`
|
|
17
|
+
+ `<span class="ui-identity__logo" aria-hidden="true"><span>${esc(symbol.slice(0, 1))}</span>${logo ? `<img src="${esc(logo)}" alt="">` : ''}</span>`
|
|
18
|
+
+ `<span class="ui-identity__symbol">${esc(symbol)}</span><span class="ui-identity__name">${esc(name)}</span></${tag}>`;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const initializedImages = new WeakSet();
|
|
22
|
+
|
|
23
|
+
// Images keep their fallback underneath; no inline handlers in server-rendered markup.
|
|
24
|
+
export function initRowIdentity(root = document) {
|
|
25
|
+
root.querySelectorAll('.ui-identity__logo img').forEach(img => {
|
|
26
|
+
if (img.complete && !img.naturalWidth) img.hidden = true;
|
|
27
|
+
if (initializedImages.has(img)) return;
|
|
28
|
+
initializedImages.add(img);
|
|
29
|
+
img.addEventListener('error', () => { img.hidden = true; });
|
|
30
|
+
img.addEventListener('load', () => { img.hidden = false; });
|
|
31
|
+
});
|
|
32
|
+
}
|
package/src/index.css
CHANGED
package/src/index.js
CHANGED
|
@@ -21,3 +21,7 @@ export * from './components/stat.js';
|
|
|
21
21
|
export * from './assets/icons.js';
|
|
22
22
|
export * from './assets/brand.js';
|
|
23
23
|
export * from './motion.js';
|
|
24
|
+
|
|
25
|
+
export * from './components/table-values.js';
|
|
26
|
+
export * from './components/segmented.js';
|
|
27
|
+
export * from './components/filter-bar.js';
|
package/src/inline.js
CHANGED
|
@@ -25,7 +25,7 @@ export const baseCss = read('styles/base.css');
|
|
|
25
25
|
// Canonical topbar (same class names the portal already uses).
|
|
26
26
|
export const topbarCss = read('styles/topbar.css');
|
|
27
27
|
|
|
28
|
-
// Individual
|
|
28
|
+
// Individual sheets; successPanel() needs both styles.callout and styles.success.
|
|
29
29
|
export const styles = {
|
|
30
30
|
base: baseCss,
|
|
31
31
|
reducedMotion: read('styles/reduced-motion.css'),
|
|
@@ -45,6 +45,7 @@ export const styles = {
|
|
|
45
45
|
confirm: read('styles/confirm.css'),
|
|
46
46
|
commandPalette: read('styles/command-palette.css'),
|
|
47
47
|
table: read('styles/table.css'),
|
|
48
|
+
filterBar: read('styles/filter-bar.css'),
|
|
48
49
|
pagination: read('styles/pagination.css'),
|
|
49
50
|
stat: read('styles/stat.css'),
|
|
50
51
|
empty: read('styles/empty.css'),
|
|
@@ -79,6 +80,7 @@ export const cssText = [
|
|
|
79
80
|
styles.confirm,
|
|
80
81
|
styles.commandPalette,
|
|
81
82
|
styles.table,
|
|
83
|
+
styles.filterBar,
|
|
82
84
|
styles.pagination,
|
|
83
85
|
styles.stat,
|
|
84
86
|
styles.empty,
|
package/src/styles/callout.css
CHANGED
|
@@ -181,14 +181,10 @@
|
|
|
181
181
|
background: var(--glow-green); /* ring-gap: inherit — translucent wash retains the opaque containing gap. */
|
|
182
182
|
}
|
|
183
183
|
.ui-success__check {
|
|
184
|
+
--sx-green: var(--chip-success-ink);
|
|
184
185
|
width: 48px; height: 48px;
|
|
185
|
-
border-radius: 50%;
|
|
186
|
-
background: var(--green);
|
|
187
|
-
color: var(--signal-contrast);
|
|
188
|
-
display: grid;
|
|
189
|
-
place-items: center;
|
|
190
186
|
margin: 0 auto 14px;
|
|
191
187
|
}
|
|
192
|
-
.ui-success__check
|
|
188
|
+
.ui-success__check .ui-sx__check { width: 100%; height: 100%; }
|
|
193
189
|
.ui-success__title { color: var(--strong); font-size: var(--text-lg); font-weight: var(--weight-semibold); margin-bottom: 5px; }
|
|
194
190
|
.ui-success__sub { color: var(--text); font-size: var(--text-base); }
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
.ui-filter-bar { border: 0; padding: 0; margin: 0; display: flex; flex-wrap: wrap; align-items: center; gap: var(--space-2); min-width: 0; }
|
|
2
|
+
.ui-filter-bar__legend { position: absolute; width: 1px; height: 1px; padding: 0; overflow: hidden; clip-path: inset(50%); }
|
|
3
|
+
.ui-filter-bar__chip { display: inline-flex; align-items: center; border: 1px solid var(--border-strong); border-radius: var(--radius-sm); padding: var(--space-1); margin: 0; min-width: 0; }
|
|
4
|
+
.ui-filter-bar__chip .ui-dropdown__trigger { border: 0; background: transparent; padding: var(--space-1) var(--space-2); }
|
|
5
|
+
.ui-filter-bar__remove { border: 0; background: transparent; color: var(--text); font: inherit; text-align: center; font-size: var(--text-lg); padding: 0; min-width: 24px; min-height: 24px; border-radius: var(--radius-xs); cursor: pointer; }
|
|
6
|
+
.ui-filter-bar__remove:focus-visible,
|
|
7
|
+
.ui-filter-bar:focus-visible { outline: 2px solid transparent; box-shadow: var(--ring); }
|
|
8
|
+
.ui-filter-bar button:disabled { /* muted-ink: state — disabled filters */ color: var(--disabled-ink); cursor: not-allowed; }
|
|
9
|
+
|
|
10
|
+
.ui-filter-bar .ui-dropdown__pre, .ui-filter-bar .ui-dropdown__value { color: inherit; }
|
package/src/styles/segmented.css
CHANGED
|
@@ -55,3 +55,10 @@
|
|
|
55
55
|
box-shadow: 0 0 0 2px var(--surface), 0 0 0 4px var(--swatch, var(--accent));
|
|
56
56
|
}
|
|
57
57
|
.ui-accent-picker button:focus-visible { outline: 2px solid transparent; box-shadow: var(--ring); }
|
|
58
|
+
|
|
59
|
+
.ui-seg--underline { background: transparent; --ring-gap: inherit; border-radius: 0; border-bottom: 1px solid var(--border); padding: var(--space-1); overflow: auto; max-width: 100%; }
|
|
60
|
+
.ui-seg--underline button { border-radius: var(--radius-xs); border-bottom: 2px solid transparent; padding: var(--space-3); white-space: nowrap; }
|
|
61
|
+
.ui-seg--underline button.is-active,
|
|
62
|
+
.ui-seg--underline button[aria-pressed="true"] { background: transparent; border-bottom-color: var(--accent); }
|
|
63
|
+
.ui-seg button:focus-visible { outline: 2px solid transparent; box-shadow: var(--ring); }
|
|
64
|
+
.ui-seg button:disabled { /* muted-ink: state — unavailable choice */ color: var(--disabled-ink); cursor: not-allowed; }
|
package/src/styles/success.css
CHANGED
|
@@ -11,11 +11,13 @@
|
|
|
11
11
|
* the foot. Markup comes from success() in components/index (re-exported).
|
|
12
12
|
* ========================================================================== */
|
|
13
13
|
|
|
14
|
-
.ui-sx {
|
|
15
|
-
--sx-green: var(--green);
|
|
14
|
+
.ui-sx, .ui-success__check {
|
|
16
15
|
--sx-tint: color-mix(in srgb, var(--green) 18%, transparent);
|
|
17
16
|
--sx-track: color-mix(in srgb, var(--green) 32%, transparent);
|
|
17
|
+
}
|
|
18
18
|
|
|
19
|
+
.ui-sx {
|
|
20
|
+
--sx-green: var(--green);
|
|
19
21
|
position: relative;
|
|
20
22
|
overflow: hidden;
|
|
21
23
|
border-radius: 20px;
|
package/src/styles/table.css
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/* ============================================================================
|
|
2
|
-
* Table — .ui-table (
|
|
2
|
+
* Table — .ui-table (hairline rows, opaque base surface)
|
|
3
3
|
* Row modifiers: .is-dead (revoked/struck), right-aligned .ui-table__act
|
|
4
4
|
* ========================================================================== */
|
|
5
5
|
|
|
@@ -26,7 +26,7 @@
|
|
|
26
26
|
}
|
|
27
27
|
.ui-table tr:last-child td { border-bottom: 0; }
|
|
28
28
|
.ui-table td.ui-table__title { color: var(--strong); font-weight: var(--weight-medium); width: 99%; white-space: normal; }
|
|
29
|
-
.ui-table th:last-child
|
|
29
|
+
.ui-table th:last-child { padding-right: 0; }
|
|
30
30
|
.ui-table td.ui-table__act { padding-right: 0; text-align: right; }
|
|
31
31
|
.ui-table tr.is-dead td { /* muted-ink: state — revoked row */ color: var(--muted); }
|
|
32
32
|
.ui-table tr.is-dead td.ui-table__title { text-decoration: line-through; }
|
|
@@ -50,7 +50,7 @@
|
|
|
50
50
|
|
|
51
51
|
/* Data-table modifiers, composable: .ui-table--dense --zebra --hover.
|
|
52
52
|
* --dense --space-3 across, --space-2 down — one step in from the base table
|
|
53
|
-
* --zebra
|
|
53
|
+
* --zebra legacy spacing and 2px header rule; the base-surface rule below removes stripes
|
|
54
54
|
* Cell helpers: .ui-table__num (right, tabular), .ui-table__code.
|
|
55
55
|
* why: docs/specification.md#spacing-and-rhythm
|
|
56
56
|
* held by: stories/table-rhythm.test.js */
|
|
@@ -83,3 +83,45 @@
|
|
|
83
83
|
|
|
84
84
|
/* The empty state (.ui-empty) — shown when a table/list has no rows — now
|
|
85
85
|
lives in styles/empty.css as a standalone component (illustration support). */
|
|
86
|
+
|
|
87
|
+
/* White in light, base canvas in dark; never a grey or tinted data surface. */
|
|
88
|
+
.ui-table { background: var(--table-bg); --ring-gap: var(--table-bg); }
|
|
89
|
+
.ui-table--zebra tbody tr:nth-child(even) td,
|
|
90
|
+
.ui-table--hover:not(.ui-table--zebra) tbody tr:hover td,
|
|
91
|
+
.ui-table--zebra tbody tr:hover td,
|
|
92
|
+
.ui-table--zebra.ui-table--hover tbody tr:hover td {
|
|
93
|
+
background: var(--table-bg); --ring-gap: var(--table-bg);
|
|
94
|
+
}
|
|
95
|
+
.ui-table tbody tr:hover { outline: 1px solid var(--border-strong); outline-offset: -1px; }
|
|
96
|
+
.ui-table td.ui-table__num { text-align: right; }
|
|
97
|
+
.ui-table--compact td { padding: var(--space-1) var(--space-3); font-size: var(--text-sm); line-height: var(--leading-snug); height: 33px; }
|
|
98
|
+
.ui-table--compact th { padding: var(--space-2) var(--space-3); }
|
|
99
|
+
.ui-table-scroll { overflow: auto; max-height: var(--ui-table-height, 70vh); padding: 0 var(--space-1); }
|
|
100
|
+
.ui-table-scroll:focus-visible { outline: 2px solid transparent; box-shadow: var(--ring); }
|
|
101
|
+
.ui-table--sticky { border-collapse: separate; border-spacing: 0; }
|
|
102
|
+
.ui-table--sticky thead th { position: sticky; top: 0; z-index: var(--z-sticky); background: var(--table-bg); --ring-gap: var(--table-bg); padding-top: var(--space-2); }
|
|
103
|
+
.ui-table--pinned .ui-table__identity, .ui-table--pinned .ui-table__selection { position: sticky; left: 0; z-index: var(--z-base); background: var(--table-bg); --ring-gap: var(--table-bg); border-right: 1px solid var(--border); }
|
|
104
|
+
.ui-table--pinned thead .ui-table__identity, .ui-table--pinned thead .ui-table__selection { z-index: calc(var(--z-sticky) + 1); }
|
|
105
|
+
.ui-value, .ui-delta { white-space: nowrap; font-variant-numeric: tabular-nums; }
|
|
106
|
+
.ui-value__unit { margin-left: var(--space-1); font-size: var(--text-xs); color: var(--text); }
|
|
107
|
+
.ui-delta--success { color: var(--chip-success-ink); }
|
|
108
|
+
.ui-delta--danger { color: var(--chip-danger-ink); }
|
|
109
|
+
.ui-identity { min-height: 24px; display: inline-flex; align-items: center; gap: var(--space-2); color: var(--text); text-decoration: none; }
|
|
110
|
+
.ui-identity__symbol { font-family: var(--font-mono); font-weight: var(--weight-semibold); }
|
|
111
|
+
.ui-identity__name { white-space: normal; max-width: var(--panel-sm); }
|
|
112
|
+
.ui-identity__logo { width: 24px; height: 24px; flex: 0 0 24px; position: relative; display: grid; place-items: center; border: 1px solid var(--border); border-radius: var(--radius-xs); font-size: var(--text-xs); }
|
|
113
|
+
.ui-identity__logo img { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: contain; }
|
|
114
|
+
.ui-identity__logo img[hidden] { display: none; /* motion: still — a failed resource is replaced without animating the error. */ }
|
|
115
|
+
a.ui-identity:hover { text-decoration: underline; }
|
|
116
|
+
.ui-identity:focus-visible { outline: 2px solid transparent; box-shadow: var(--ring); border-radius: var(--radius-xs); }
|
|
117
|
+
@media (max-width: 720px) {
|
|
118
|
+
.ui-table--pinned .ui-table__identity { width: 1%; }
|
|
119
|
+
.ui-table--pinned .ui-identity__logo { display: none; }
|
|
120
|
+
.ui-table--pinned .ui-identity__name { position: absolute; width: 1px; height: 1px; overflow: hidden; clip-path: inset(50%); }
|
|
121
|
+
.ui-table--pinned .ui-identity { max-width: var(--panel-sm); }
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/* One width reserves the selection track and offsets the adjacent identity. */
|
|
125
|
+
.ui-table--pinned { --ui-table-selection-width: var(--space-12); }
|
|
126
|
+
.ui-table--pinned .ui-table__selection { box-sizing: border-box; width: var(--ui-table-selection-width); min-width: var(--ui-table-selection-width); max-width: var(--ui-table-selection-width); padding-inline: 0; text-align: center; }
|
|
127
|
+
.ui-table--pinned .ui-table__selection + .ui-table__identity { left: var(--ui-table-selection-width); }
|
package/src/tokens/tokens.css
CHANGED
|
@@ -138,6 +138,7 @@
|
|
|
138
138
|
below the floating step; the floating step itself adds --elev-drop under its inner
|
|
139
139
|
line and nothing else in the kit does. why: docs/specification.md#elevation */
|
|
140
140
|
--bg: #0e0d14;
|
|
141
|
+
--table-bg: var(--bg);
|
|
141
142
|
--surface-2: #161520; /* SUNKEN — fields, tracks, disabled, code */
|
|
142
143
|
--surface: #211e2d; /* the card */
|
|
143
144
|
--bg-elevated: #2a2639; /* FLOATING — menus, panels, drawer, modal, toasts */
|
|
@@ -284,6 +285,7 @@
|
|
|
284
285
|
white a card already was, so light moves DOWN and keeps white for the top step.
|
|
285
286
|
why: docs/specification.md#elevation */
|
|
286
287
|
--bg: #eef0f5;
|
|
288
|
+
--table-bg: #ffffff;
|
|
287
289
|
--surface-2: #e3e6ee; /* SUNKEN — fields, tracks, disabled, code */
|
|
288
290
|
--surface: #f8f9fc; /* the card */
|
|
289
291
|
--bg-elevated: #ffffff; /* FLOATING — menus, panels, drawer, modal, toasts */
|
|
@@ -430,6 +432,9 @@
|
|
|
430
432
|
.ui-snippet,
|
|
431
433
|
.ui-snippet--reveal,
|
|
432
434
|
.ui-sx,
|
|
435
|
+
.ui-table,
|
|
436
|
+
.ui-table--sticky thead th,
|
|
437
|
+
.ui-table--pinned .ui-table__identity, .ui-table--pinned .ui-table__selection,
|
|
433
438
|
.ui-table--hover:not(.ui-table--zebra) tbody tr:hover td,
|
|
434
439
|
.ui-table--zebra tbody tr:nth-child(even) td,
|
|
435
440
|
.ui-table--zebra tbody tr:hover td,
|