@apliteni/apliteni-ui 0.115.0 → 0.117.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.
@@ -68,3 +68,5 @@
68
68
  **Don't:** Put text on grey fields, chips, headers or footers.
69
69
 
70
70
  **Except:** The sidebar rail's current row. Its mark may be neither an accent nor an edge — an outline there means focus — and in the page shell the rail is the card surface already. It takes the quiet fill a rung off its rail, label and all. Artur, r34 of [#475](https://github.com/apliteni/apliteni-ui/issues/475).
71
+
72
+ **Except:** A status chip carries a soft fill. Where its tone reports no verdict it takes a light tint of muted ink that leaves the panel in one direction per theme, so the neutral tones read as the same family as the coloured ones.
@@ -60,6 +60,20 @@
60
60
 
61
61
  **Don't:** Hide units in muted text or make readers guess what an abbreviation means.
62
62
 
63
+ ## Size the table to its content
64
+
65
+ <!-- rule: content-width -->
66
+
67
+ **Why:** Large gaps separate labels from values.
68
+
69
+ **Rule:** Limit each column to its longest cell plus the cell inset.
70
+
71
+ **Do:** Keep amounts beside their references in a two-column table.
72
+
73
+ **Don't:** Stretch short tables across the page.
74
+
75
+ **Except:** A text column intended to grow, such as a name or title, takes the remaining width.
76
+
63
77
  ## Scroll instead of squeezing
64
78
 
65
79
  <!-- rule: overflow -->
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@apliteni/apliteni-ui",
3
- "version": "0.115.0",
3
+ "version": "0.117.0",
4
4
  "workspaces": [
5
5
  "react"
6
6
  ],
@@ -317,6 +317,12 @@
317
317
  transition: none;
318
318
  }
319
319
  }
320
+ .rx-table-pager:has(.ui-pager) {
321
+ margin-top: var(--space-4);
322
+ }
323
+ .rx-table-pager:not(:has(.ui-pager)) {
324
+ display: none;
325
+ }
320
326
 
321
327
  /* src/DatePicker.css */
322
328
  .ui-datepicker__panel {
@@ -23,7 +23,7 @@ declare const Switch: react.ForwardRefExoticComponent<Omit<InputHTMLAttributes<H
23
23
  } & react.RefAttributes<HTMLInputElement>>;
24
24
 
25
25
  type ButtonOptions = {
26
- variant?: 'primary' | 'secondary' | 'ghost' | 'danger';
26
+ variant?: 'primary' | 'secondary' | 'ghost' | 'danger' | 'link';
27
27
  size?: 'xs' | 'sm' | 'md' | 'lg';
28
28
  icon?: string;
29
29
  /** Decorative caller artwork, before the label; takes precedence over icon. */
@@ -581,6 +581,7 @@ type FilterBarProps = {
581
581
  filters: Filter[];
582
582
  label?: string;
583
583
  clearLabel?: string;
584
+ clearVariant?: 'ghost' | 'secondary' | 'link';
584
585
  disabled?: boolean;
585
586
  busy?: boolean;
586
587
  add?: AddFilter[];
@@ -590,7 +591,7 @@ type FilterBarProps = {
590
591
  onChange: (id: string, value: string | undefined) => void;
591
592
  onAdd?: (id: string, value: string | undefined) => void;
592
593
  };
593
- declare function FilterBar({ filters, label, clearLabel, add, addLabel, disabled, busy, onRemove, onClear, onChange, onAdd }: FilterBarProps): react.JSX.Element;
594
+ declare function FilterBar({ filters, label, clearLabel, clearVariant, add, addLabel, disabled, busy, onRemove, onClear, onChange, onAdd }: FilterBarProps): react.JSX.Element;
594
595
 
595
596
  type KeyValueRow = {
596
597
  label: string;
@@ -945,7 +945,7 @@ function rowId(uid, groups, item) {
945
945
  }
946
946
 
947
947
  // src/DataTable.tsx
948
- import { useMemo as useMemo2, useState as useState5 } from "react";
948
+ import { useCallback as useCallback2, useEffect as useEffect5, useMemo as useMemo2, useRef as useRef9, useState as useState5 } from "react";
949
949
  import { DEFAULT_PAGE_SIZE as DEFAULT_PAGE_SIZE2 } from "@apliteni/apliteni-ui";
950
950
 
951
951
  // src/Pagination.tsx
@@ -1209,6 +1209,33 @@ function DataTable({
1209
1209
  hasMore = false
1210
1210
  }) {
1211
1211
  const scrollable = stickyHeader || pinnedIdentity;
1212
+ const table = useRef9(null);
1213
+ const [pagerHost, setPagerHost] = useState5(null);
1214
+ const holdPagerBox = useCallback2(
1215
+ (box) => setPagerHost(box?.parentElement ?? null),
1216
+ []
1217
+ );
1218
+ const [strip2, setStrip] = useState5();
1219
+ useEffect5(() => {
1220
+ const el = table.current;
1221
+ const host = pagerHost;
1222
+ if (!el || !host || typeof ResizeObserver === "undefined") {
1223
+ setStrip(void 0);
1224
+ return;
1225
+ }
1226
+ const measure = () => {
1227
+ const rect = el.getBoundingClientRect();
1228
+ const box = host.getBoundingClientRect();
1229
+ const left = box.left + host.clientLeft + (Number.parseFloat(getComputedStyle(host).paddingLeft) || 0);
1230
+ const inset = Math.max(0, rect.left - left);
1231
+ const width = rect.right - left - inset;
1232
+ setStrip(width > 0 ? { width, inset } : void 0);
1233
+ };
1234
+ const observer = new ResizeObserver(measure);
1235
+ observer.observe(el);
1236
+ measure();
1237
+ return () => observer.disconnect();
1238
+ }, [pagerHost, columns, rows]);
1212
1239
  const [localSort, setLocalSort] = useState5(
1213
1240
  { key: columns.find((c) => c.sortable)?.key, dir: -1 }
1214
1241
  );
@@ -1271,6 +1298,7 @@ function DataTable({
1271
1298
  children: /* @__PURE__ */ jsxs13(
1272
1299
  "table",
1273
1300
  {
1301
+ ref: table,
1274
1302
  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(" "),
1275
1303
  "aria-busy": loading || void 0,
1276
1304
  children: [
@@ -1328,23 +1356,37 @@ function DataTable({
1328
1356
  )
1329
1357
  }
1330
1358
  ),
1331
- pager ? /* @__PURE__ */ jsx17(
1332
- Pagination,
1333
- {
1334
- page,
1335
- pageSize: size,
1336
- total: owned ? ordered.length : total ?? null,
1337
- hasMore,
1338
- pageSizes,
1339
- loading,
1340
- ...pagerLabel === void 0 ? {} : { label: pagerLabel },
1341
- onPageChange: goTo,
1342
- onPageSizeChange: (s) => {
1343
- if (pageSize === void 0) setLocalSize(s);
1344
- onPageSizeChange?.(s);
1345
- if (owned) setLocalPage(1);
1359
+ pager ? (
1360
+ // The strip that pages a table may not be wider than the rows it pages (#504).
1361
+ // The two are siblings, and no selector sizes one to the other, so the width is
1362
+ // measured: a cap rather than a width, so a pager wider than the table wraps
1363
+ // inside it and a table wider than the room still leaves the strip in the room.
1364
+ /* @__PURE__ */ jsx17(
1365
+ "div",
1366
+ {
1367
+ className: "rx-table-pager",
1368
+ ref: holdPagerBox,
1369
+ style: strip2 ? { maxWidth: strip2.width, marginInlineStart: strip2.inset || void 0 } : void 0,
1370
+ children: /* @__PURE__ */ jsx17(
1371
+ Pagination,
1372
+ {
1373
+ page,
1374
+ pageSize: size,
1375
+ total: owned ? ordered.length : total ?? null,
1376
+ hasMore,
1377
+ pageSizes,
1378
+ loading,
1379
+ ...pagerLabel === void 0 ? {} : { label: pagerLabel },
1380
+ onPageChange: goTo,
1381
+ onPageSizeChange: (s) => {
1382
+ if (pageSize === void 0) setLocalSize(s);
1383
+ onPageSizeChange?.(s);
1384
+ if (owned) setLocalPage(1);
1385
+ }
1386
+ }
1387
+ )
1346
1388
  }
1347
- }
1389
+ )
1348
1390
  ) : null
1349
1391
  ] });
1350
1392
  }
@@ -1424,10 +1466,10 @@ function Denied({
1424
1466
 
1425
1467
  // src/DatePicker.tsx
1426
1468
  import {
1427
- useCallback as useCallback2,
1428
- useEffect as useEffect5,
1469
+ useCallback as useCallback3,
1470
+ useEffect as useEffect6,
1429
1471
  useMemo as useMemo3,
1430
- useRef as useRef9,
1472
+ useRef as useRef10,
1431
1473
  useState as useState6
1432
1474
  } from "react";
1433
1475
  import { jsx as jsx19, jsxs as jsxs15 } from "react/jsx-runtime";
@@ -1440,7 +1482,7 @@ var DAY_RE = /^(\d{4})-(\d{2})-(\d{2})$/;
1440
1482
  var PHONE_MAX = 560;
1441
1483
  function usePhone() {
1442
1484
  const [phone, setPhone] = useState6(false);
1443
- useEffect5(() => {
1485
+ useEffect6(() => {
1444
1486
  const query = typeof window === "undefined" ? null : window.matchMedia?.(`(max-width: ${PHONE_MAX}px)`);
1445
1487
  if (!query) return;
1446
1488
  const read = () => setPhone(query.matches);
@@ -1535,9 +1577,9 @@ function DatePicker({
1535
1577
  const picksRange = mode === "range" || mode === "day-range";
1536
1578
  const phone = usePhone();
1537
1579
  const asSheet = sheet ?? phone;
1538
- const root = useRef9(null);
1539
- const trigger = useRef9(null);
1540
- const grid = useRef9(null);
1580
+ const root = useRef10(null);
1581
+ const trigger = useRef10(null);
1582
+ const grid = useRef10(null);
1541
1583
  const [selfOpen, setSelfOpen] = useState6(defaultOpen);
1542
1584
  const open = openProp ?? selfOpen;
1543
1585
  const [selfValue, setSelfValue] = useState6(defaultValue);
@@ -1555,7 +1597,7 @@ function DatePicker({
1555
1597
  weekdayShort: new Intl.DateTimeFormat(locale, { weekday: "short", timeZone: "UTC" })
1556
1598
  }), [locale]);
1557
1599
  const monthLabels = useMemo3(() => evenMonthLabels(names.monthShort), [names]);
1558
- const at = useCallback2((index) => new Date(
1600
+ const at = useCallback3((index) => new Date(
1559
1601
  grain === "month" ? Date.UTC(Math.floor(index / 12), index % 12, 1) : index * DAY_MS
1560
1602
  ), [grain]);
1561
1603
  const lo = toIndex(min, grain, "start");
@@ -1574,19 +1616,19 @@ function DatePicker({
1574
1616
  }, [today, grain]);
1575
1617
  const anchor = toIndex(picked, grain) ?? toIndex(picksRange ? span.start ?? span.end : null, grain) ?? todayIndex;
1576
1618
  const [cursor, setCursor] = useState6(() => clamp(anchor, lo, hi));
1577
- const land = useRef9(false);
1578
- const setOpen = useCallback2((next) => {
1619
+ const land = useRef10(false);
1620
+ const setOpen = useCallback3((next) => {
1579
1621
  if (openProp === void 0) setSelfOpen(next);
1580
1622
  onOpenChange?.(next);
1581
1623
  }, [openProp, onOpenChange]);
1582
- const close = useRef9(setOpen);
1624
+ const close = useRef10(setOpen);
1583
1625
  useIsoLayoutEffect(() => {
1584
1626
  close.current = setOpen;
1585
1627
  });
1586
- useEffect5(() => {
1628
+ useEffect6(() => {
1587
1629
  if (open) setCursor(clamp(anchor, lo, hi));
1588
1630
  }, [open]);
1589
- useEffect5(() => {
1631
+ useEffect6(() => {
1590
1632
  if (!open || asSheet) return;
1591
1633
  const doc = root.current?.ownerDocument ?? document;
1592
1634
  const onClick = (e) => {
@@ -1612,7 +1654,7 @@ function DatePicker({
1612
1654
  const columns = grain === "month" ? 3 : 7;
1613
1655
  const page = grain === "month" ? Math.floor(cursor / 12) : monthOf(cursor);
1614
1656
  const caption = grain === "month" ? String(page) : names.monthYear.format(at(firstDayOf(page)));
1615
- const periodName = useCallback2((index) => grain === "month" ? names.monthYear.format(at(index)) : `${names.weekday.format(at(index))} ${names.day.format(at(index))}`, [grain, names, at]);
1657
+ const periodName = useCallback3((index) => grain === "month" ? names.monthYear.format(at(index)) : `${names.weekday.format(at(index))} ${names.day.format(at(index))}`, [grain, names, at]);
1616
1658
  const count = grain === "month" ? 12 : daysInMonth(page);
1617
1659
  const first = grain === "month" ? page * 12 : firstDayOf(page);
1618
1660
  const last = first + count - 1;
@@ -1895,10 +1937,10 @@ function DatePicker({
1895
1937
  // src/Dropdown.tsx
1896
1938
  import {
1897
1939
  Fragment as Fragment4,
1898
- useCallback as useCallback3,
1899
- useEffect as useEffect6,
1940
+ useCallback as useCallback4,
1941
+ useEffect as useEffect7,
1900
1942
  useId as useId7,
1901
- useRef as useRef10,
1943
+ useRef as useRef11,
1902
1944
  useState as useState7
1903
1945
  } from "react";
1904
1946
  import {
@@ -1975,19 +2017,19 @@ function Dropdown({
1975
2017
  const [pick, setPick] = useState7(null);
1976
2018
  const [query, setQuery] = useState7(() => search && search !== true ? search.query || "" : "");
1977
2019
  const [activeKey, setActiveKey] = useState7(null);
1978
- const root = useRef10(null);
1979
- const panel = useRef10(null);
1980
- const trigger = useRef10(null);
1981
- const field = useRef10(null);
1982
- const landOn = useRef10(null);
1983
- const at = useRef10("");
2020
+ const root = useRef11(null);
2021
+ const panel = useRef11(null);
2022
+ const trigger = useRef11(null);
2023
+ const field = useRef11(null);
2024
+ const landOn = useRef11(null);
2025
+ const at = useRef11("");
1984
2026
  const auto = useId7().replace(/:/g, "");
1985
2027
  const uid = id ?? auto;
1986
- const setOpen = useCallback3((next) => {
2028
+ const setOpen = useCallback4((next) => {
1987
2029
  if (openProp === void 0) setSelfOpen(next);
1988
2030
  onOpenChange?.(next);
1989
2031
  }, [openProp, onOpenChange]);
1990
- const close = useRef10(setOpen);
2032
+ const close = useRef11(setOpen);
1991
2033
  useIsoLayoutEffect(() => {
1992
2034
  close.current = setOpen;
1993
2035
  });
@@ -2040,7 +2082,7 @@ function Dropdown({
2040
2082
  stop.forEach((off) => off());
2041
2083
  };
2042
2084
  }, [open]);
2043
- const opened = useRef10(false);
2085
+ const opened = useRef11(false);
2044
2086
  useIsoLayoutEffect(() => {
2045
2087
  const el = panel.current;
2046
2088
  if (open) {
@@ -2079,7 +2121,7 @@ function Dropdown({
2079
2121
  const given = rows.find((it) => it.selected);
2080
2122
  const givenKey = given ? keyOf(given) : null;
2081
2123
  const pickedKey = pick && pick.against === givenKey ? pick.key : null;
2082
- useEffect6(() => {
2124
+ useEffect7(() => {
2083
2125
  if (pick && pick.against !== givenKey) setPick(null);
2084
2126
  }, [givenKey, pick]);
2085
2127
  const picked = pickedKey != null ? rows.find((it) => keyOf(it) === pickedKey) : void 0;
@@ -2093,7 +2135,7 @@ function Dropdown({
2093
2135
  const rowIds = /* @__PURE__ */ new Map();
2094
2136
  rows.forEach((it, i) => rowIds.set(it, `${uid}-opt-${i}`));
2095
2137
  const listId = `${uid}-list`;
2096
- useEffect6(() => {
2138
+ useEffect7(() => {
2097
2139
  if (!open) return;
2098
2140
  const mine = () => close.current(false);
2099
2141
  for (const other of [...openNow]) other();
@@ -2102,7 +2144,7 @@ function Dropdown({
2102
2144
  openNow.delete(mine);
2103
2145
  };
2104
2146
  }, [open]);
2105
- useEffect6(() => {
2147
+ useEffect7(() => {
2106
2148
  if (!open) return;
2107
2149
  const doc = root.current?.ownerDocument ?? document;
2108
2150
  const onClick = (e) => {
@@ -2567,15 +2609,15 @@ function Segmented({ options, value, onChange, label, appearance = "pill", size,
2567
2609
  }
2568
2610
 
2569
2611
  // src/FilterBar.tsx
2570
- import { useRef as useRef11, useState as useState8 } from "react";
2612
+ import { useRef as useRef12, useState as useState8 } from "react";
2571
2613
  import { filterChipText, filterChipName, filterChipUnset, filterChipItems, focusNextStop } from "@apliteni/apliteni-ui";
2572
2614
  import { Fragment as Fragment6, jsx as jsx24, jsxs as jsxs19 } from "react/jsx-runtime";
2573
2615
  var SEARCH_AT = 10;
2574
2616
  var isRow2 = (entry) => entry !== "---" && !entry.separator;
2575
- function FilterBar({ filters, label = "Filters", clearLabel = "Clear all", add, addLabel = "Filter", disabled = false, busy = false, onRemove, onClear, onChange, onAdd }) {
2576
- const bar = useRef11(null);
2577
- const focused = useRef11(null);
2578
- const landing = useRef11(null);
2617
+ function FilterBar({ filters, label = "Filters", clearLabel = "Clear all", clearVariant = "ghost", add, addLabel = "Filter", disabled = false, busy = false, onRemove, onClear, onChange, onAdd }) {
2618
+ const bar = useRef12(null);
2619
+ const focused = useRef12(null);
2620
+ const landing = useRef12(null);
2579
2621
  const [opened, setOpened] = useState8({});
2580
2622
  const [adding, setAdding] = useState8(false);
2581
2623
  const blocked = disabled || busy;
@@ -2693,7 +2735,7 @@ function FilterBar({ filters, label = "Filters", clearLabel = "Clear all", add,
2693
2735
  ) }),
2694
2736
  filters.length > 0 && /* @__PURE__ */ jsx24("span", { "data-filter-clear": "", onFocus: () => {
2695
2737
  focused.current = { id: null, index: 0 };
2696
- }, children: /* @__PURE__ */ jsx24(Button, { size: "sm", variant: "ghost", onClick: () => {
2738
+ }, children: /* @__PURE__ */ jsx24(Button, { size: "sm", variant: clearVariant, onClick: () => {
2697
2739
  if (!blocked) onClear();
2698
2740
  }, children: clearLabel }) })
2699
2741
  ]
@@ -2726,13 +2768,13 @@ function DrawerSection({ title, headingLevel = 3, className, children, ...rest }
2726
2768
  }
2727
2769
 
2728
2770
  // src/Snippet.tsx
2729
- import { useEffect as useEffect7, useRef as useRef12, useState as useState9 } from "react";
2771
+ import { useEffect as useEffect8, useRef as useRef13, useState as useState9 } from "react";
2730
2772
  import { Fragment as Fragment7, jsx as jsx26, jsxs as jsxs21 } from "react/jsx-runtime";
2731
2773
  function Snippet({ label = "shell", code = "", children, copy = true, reveal = false, copyLabel = "Copy code" }) {
2732
2774
  const [status, setStatus] = useState9("idle");
2733
- const request = useRef12(0);
2734
- const timer = useRef12(void 0);
2735
- useEffect7(() => {
2775
+ const request = useRef13(0);
2776
+ const timer = useRef13(void 0);
2777
+ useEffect8(() => {
2736
2778
  setStatus("idle");
2737
2779
  return () => {
2738
2780
  request.current++;
@@ -2775,15 +2817,15 @@ function Snippet({ label = "shell", code = "", children, copy = true, reveal = f
2775
2817
  }
2776
2818
 
2777
2819
  // src/Tabs.tsx
2778
- import { useEffect as useEffect8, useId as useId8, useRef as useRef13 } from "react";
2820
+ import { useEffect as useEffect9, useId as useId8, useRef as useRef14 } from "react";
2779
2821
  import { playEntrance } from "@apliteni/apliteni-ui/motion";
2780
2822
  import { Fragment as Fragment8, jsx as jsx27, jsxs as jsxs22 } from "react/jsx-runtime";
2781
2823
  function Tabs({ items, value, onChange, label }) {
2782
2824
  const id = useId8();
2783
- const previous = useRef13(value);
2784
- const activePanel = useRef13(null);
2785
- const buttons = useRef13([]);
2786
- useEffect8(() => {
2825
+ const previous = useRef14(value);
2826
+ const activePanel = useRef14(null);
2827
+ const buttons = useRef14([]);
2828
+ useEffect9(() => {
2787
2829
  if (previous.current !== value) playEntrance(activePanel.current);
2788
2830
  previous.current = value;
2789
2831
  }, [value]);
@@ -2852,14 +2894,14 @@ function Tabs({ items, value, onChange, label }) {
2852
2894
  }
2853
2895
 
2854
2896
  // src/Timeline.tsx
2855
- import { useEffect as useEffect9, useRef as useRef14 } from "react";
2897
+ import { useEffect as useEffect10, useRef as useRef15 } from "react";
2856
2898
  import { playEntrance as playEntrance2, prefersReducedMotion } from "@apliteni/apliteni-ui/motion";
2857
2899
  import { jsx as jsx28, jsxs as jsxs23 } from "react/jsx-runtime";
2858
2900
  var kindIcons = { person: "user", rule: "bolt", reversal: "refresh" };
2859
2901
  function Timeline({ events, className, ...rest }) {
2860
- const previousIds = useRef14(new Set(events.map((event) => event.id)));
2861
- const rows = useRef14(/* @__PURE__ */ new Map());
2862
- useEffect9(() => {
2902
+ const previousIds = useRef15(new Set(events.map((event) => event.id)));
2903
+ const rows = useRef15(/* @__PURE__ */ new Map());
2904
+ useEffect10(() => {
2863
2905
  if (previousIds.current.size > 0 && !prefersReducedMotion()) {
2864
2906
  for (const event of events) {
2865
2907
  if (previousIds.current.has(event.id)) continue;
@@ -2902,7 +2944,7 @@ function Timeline({ events, className, ...rest }) {
2902
2944
  }
2903
2945
 
2904
2946
  // src/Field.tsx
2905
- import { forwardRef as forwardRef6, useEffect as useEffect10, useId as useId9, useRef as useRef15, useState as useState10 } from "react";
2947
+ import { forwardRef as forwardRef6, useEffect as useEffect11, useId as useId9, useRef as useRef16, useState as useState10 } from "react";
2906
2948
  import { esc, icon as icon4 } from "@apliteni/apliteni-ui";
2907
2949
  import { jsx as jsx29, jsxs as jsxs24 } from "react/jsx-runtime";
2908
2950
  function messages(id, hint, error) {
@@ -2990,10 +3032,10 @@ function SelectField({ label, hint, error, required, className = "", children, .
2990
3032
  }
2991
3033
  function FileField({ label, hint, error, required, disabled, onFileChange, className = "", ...props }) {
2992
3034
  const id = useId9();
2993
- const input = useRef15(null);
3035
+ const input = useRef16(null);
2994
3036
  const [file, setFile] = useState10(null);
2995
3037
  const [dragging, setDragging] = useState10(false);
2996
- useEffect10(() => {
3038
+ useEffect11(() => {
2997
3039
  const form = input.current?.form;
2998
3040
  const reset = () => {
2999
3041
  setFile(null);
@@ -3052,7 +3094,7 @@ function FileField({ label, hint, error, required, disabled, onFileChange, class
3052
3094
  }
3053
3095
 
3054
3096
  // src/FileDrop.tsx
3055
- import { useRef as useRef16, useState as useState11 } from "react";
3097
+ import { useRef as useRef17, useState as useState11 } from "react";
3056
3098
  import { Fragment as Fragment9, jsx as jsx30, jsxs as jsxs25 } from "react/jsx-runtime";
3057
3099
  var MARKS = {
3058
3100
  uploading: "clock",
@@ -3086,7 +3128,7 @@ function FileDrop({
3086
3128
  children,
3087
3129
  className
3088
3130
  }) {
3089
- const input = useRef16(null);
3131
+ const input = useRef17(null);
3090
3132
  const [over, setOver] = useState11(false);
3091
3133
  const status = file?.status ?? "uploading";
3092
3134
  const word = status === "error" ? file?.error ?? WORDS.error : file?.state ?? WORDS[status];
@@ -3221,7 +3263,7 @@ var SearchField = forwardRef7(function SearchField2({ ariaLabel, className, ...p
3221
3263
  });
3222
3264
 
3223
3265
  // src/Toast.tsx
3224
- import { createContext as createContext2, useCallback as useCallback4, useContext as useContext2, useEffect as useEffect11, useLayoutEffect as useLayoutEffect4, useRef as useRef17, useState as useState12 } from "react";
3266
+ import { createContext as createContext2, useCallback as useCallback5, useContext as useContext2, useEffect as useEffect12, useLayoutEffect as useLayoutEffect4, useRef as useRef18, useState as useState12 } from "react";
3225
3267
  import { createPortal as createPortal4 } from "react-dom";
3226
3268
  import { jsx as jsx32, jsxs as jsxs27 } from "react/jsx-runtime";
3227
3269
  var Context = createContext2(null);
@@ -3232,21 +3274,21 @@ function useToast() {
3232
3274
  return push;
3233
3275
  }
3234
3276
  function Notice({ notice, remove }) {
3235
- const root = useRef17(null);
3277
+ const root = useRef18(null);
3236
3278
  const [leaving2, setLeaving] = useState12(false);
3237
- const dismissed = useRef17(false);
3238
- const remaining = useRef17(5e3);
3279
+ const dismissed = useRef18(false);
3280
+ const remaining = useRef18(5e3);
3239
3281
  const [hovered, setHovered] = useState12(false);
3240
3282
  const [focused, setFocused] = useState12(false);
3241
3283
  const paused = hovered || focused;
3242
3284
  const tone = notice.tone ?? "success";
3243
- const dismiss = useCallback4(() => {
3285
+ const dismiss = useCallback5(() => {
3244
3286
  if (dismissed.current) return;
3245
3287
  dismissed.current = true;
3246
3288
  if (typeof matchMedia === "function" && matchMedia("(prefers-reduced-motion: reduce)").matches) remove();
3247
3289
  else setLeaving(true);
3248
3290
  }, [remove]);
3249
- useEffect11(() => {
3291
+ useEffect12(() => {
3250
3292
  if (leaving2) {
3251
3293
  const element = root.current;
3252
3294
  const finish = (event) => {
@@ -3260,7 +3302,7 @@ function Notice({ notice, remove }) {
3260
3302
  };
3261
3303
  }
3262
3304
  }, [leaving2, remove]);
3263
- useEffect11(() => {
3305
+ useEffect12(() => {
3264
3306
  if (leaving2 || notice.action || paused) return;
3265
3307
  const started = Date.now();
3266
3308
  const timer = setTimeout(dismiss, remaining.current);
@@ -3338,9 +3380,9 @@ function usePublishedReach(stack2, count) {
3338
3380
  }
3339
3381
  function Toast({ children }) {
3340
3382
  const [entries, setEntries] = useState12([]);
3341
- const nextId = useRef17(0);
3342
- const stack2 = useRef17(null);
3343
- const push = useCallback4((notice) => {
3383
+ const nextId = useRef18(0);
3384
+ const stack2 = useRef18(null);
3385
+ const push = useCallback5((notice) => {
3344
3386
  const id = nextId.current++;
3345
3387
  const remove = () => setEntries((current) => current.filter((entry) => entry.id !== id));
3346
3388
  setEntries((current) => [...current, { id, notice, remove }]);
@@ -3356,11 +3398,11 @@ function Toast({ children }) {
3356
3398
  }
3357
3399
 
3358
3400
  // src/AppShell.tsx
3359
- import { useEffect as useEffect13, useId as useId11, useState as useState14 } from "react";
3401
+ import { useEffect as useEffect14, useId as useId11, useState as useState14 } from "react";
3360
3402
  import { applyTheme, paletteHotkey, prism, railCollapsed, themeIcon, themeName } from "@apliteni/apliteni-ui";
3361
3403
 
3362
3404
  // src/SidebarNav.tsx
3363
- import { forwardRef as forwardRef8, useEffect as useEffect12, useId as useId10, useState as useState13 } from "react";
3405
+ import { forwardRef as forwardRef8, useEffect as useEffect13, useId as useId10, useState as useState13 } from "react";
3364
3406
  import { ENTRANCE_FALLBACK_MS } from "@apliteni/apliteni-ui/motion";
3365
3407
  import { Fragment as Fragment10, jsx as jsx33, jsxs as jsxs28 } from "react/jsx-runtime";
3366
3408
  var badgeText = (badge) => typeof badge === "object" ? badge.text : badge;
@@ -3371,7 +3413,7 @@ function Row({ item, active, activeIs, collapsed, renderLink, sub }) {
3371
3413
  const [expanded, setExpanded] = useState13();
3372
3414
  const open = expanded ?? item.defaultOpen ?? childActive;
3373
3415
  const [entering, setEntering] = useState13(false);
3374
- useEffect12(() => {
3416
+ useEffect13(() => {
3375
3417
  if (!entering) return;
3376
3418
  const timer = setTimeout(() => setEntering(false), ENTRANCE_FALLBACK_MS);
3377
3419
  return () => clearTimeout(timer);
@@ -3487,14 +3529,14 @@ function AppShell({
3487
3529
  const [theme, setTheme] = useState14("dark");
3488
3530
  const current = path(pathname);
3489
3531
  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];
3490
- useEffect13(() => {
3532
+ useEffect14(() => {
3491
3533
  setCollapsed(railCollapsed() ?? defaultCollapsed);
3492
3534
  }, [defaultCollapsed]);
3493
- useEffect13(() => {
3535
+ useEffect14(() => {
3494
3536
  setMore(false);
3495
3537
  setSearch(false);
3496
3538
  }, [pathname]);
3497
- useEffect13(() => {
3539
+ useEffect14(() => {
3498
3540
  const root = document.documentElement;
3499
3541
  const sync2 = () => setTheme(root.getAttribute("data-theme") === "light" ? "light" : "dark");
3500
3542
  sync2();
@@ -3502,7 +3544,7 @@ function AppShell({
3502
3544
  observer.observe(root, { attributes: true, attributeFilter: ["data-theme"] });
3503
3545
  return () => observer.disconnect();
3504
3546
  }, []);
3505
- useEffect13(() => {
3547
+ useEffect14(() => {
3506
3548
  const hotkey = (event) => {
3507
3549
  if (!(event.metaKey || event.ctrlKey) || event.altKey || event.shiftKey || event.key.toLowerCase() !== "k") return;
3508
3550
  const target = event.target;
@@ -3733,7 +3775,7 @@ function ThemeToggle({ labelled = false }) {
3733
3775
  }
3734
3776
 
3735
3777
  // src/Success.tsx
3736
- import { forwardRef as forwardRef9, useEffect as useEffect14, useRef as useRef18, useState as useState15 } from "react";
3778
+ import { forwardRef as forwardRef9, useEffect as useEffect15, useRef as useRef19, useState as useState15 } from "react";
3737
3779
  import { jsx as jsx37, jsxs as jsxs32 } from "react/jsx-runtime";
3738
3780
  var SuccessCheck = forwardRef9(function SuccessCheck2({ variant = "line", className, ...rest }, ref) {
3739
3781
  const circled = variant === "circled";
@@ -3781,11 +3823,11 @@ var pieces = [
3781
3823
  ];
3782
3824
  function Countdown({ seconds, label = "Redirecting", onDone }) {
3783
3825
  const [remaining, setRemaining] = useState15(seconds);
3784
- const callback = useRef18(onDone);
3785
- useEffect14(() => {
3826
+ const callback = useRef19(onDone);
3827
+ useEffect15(() => {
3786
3828
  callback.current = onDone;
3787
3829
  }, [onDone]);
3788
- useEffect14(() => {
3830
+ useEffect15(() => {
3789
3831
  let count = seconds;
3790
3832
  const timer = setInterval(() => {
3791
3833
  count -= 1;
@@ -3857,12 +3899,12 @@ var Success = forwardRef9(function Success2({
3857
3899
 
3858
3900
  // src/Chart.tsx
3859
3901
  import {
3860
- useCallback as useCallback5,
3861
- useEffect as useEffect15,
3902
+ useCallback as useCallback6,
3903
+ useEffect as useEffect16,
3862
3904
  useId as useId12,
3863
3905
  useLayoutEffect as useLayoutEffect5,
3864
3906
  useMemo as useMemo4,
3865
- useRef as useRef19,
3907
+ useRef as useRef20,
3866
3908
  useState as useState16
3867
3909
  } from "react";
3868
3910
  import { CHART_FLOOR, bridgeWalk, chartScale, pointTarget } from "@apliteni/apliteni-ui";
@@ -3905,7 +3947,7 @@ function barPath(m) {
3905
3947
  }
3906
3948
  function useTapFloor() {
3907
3949
  const [floor, setFloor] = useState16(TARGET);
3908
- useEffect15(() => {
3950
+ useEffect16(() => {
3909
3951
  const query = typeof window === "undefined" ? null : window.matchMedia?.(`(max-width: ${PHONE_MAX2}px) and (pointer: coarse)`);
3910
3952
  if (!query) return;
3911
3953
  const read = () => setFloor(query.matches ? TAP_FLOOR : TARGET);
@@ -4028,16 +4070,16 @@ function Chart(props) {
4028
4070
  const plotHeight = height ?? HEIGHT[variant];
4029
4071
  const showTable = table ?? !spark;
4030
4072
  const uid = useId12().replace(/[^\w-]/g, "");
4031
- const frameEl = useRef19(null);
4032
- const scroller = useRef19(null);
4033
- const plot = useRef19(null);
4034
- const tipEl = useRef19(null);
4035
- const svgEl = useRef19(null);
4036
- const ignoreMouseUntil = useRef19(0);
4037
- const touchMoved = useRef19(false);
4038
- const dismissed = useRef19(null);
4039
- const opening = useRef19(false);
4040
- const pointerFocus = useRef19(false);
4073
+ const frameEl = useRef20(null);
4074
+ const scroller = useRef20(null);
4075
+ const plot = useRef20(null);
4076
+ const tipEl = useRef20(null);
4077
+ const svgEl = useRef20(null);
4078
+ const ignoreMouseUntil = useRef20(0);
4079
+ const touchMoved = useRef20(false);
4080
+ const dismissed = useRef20(null);
4081
+ const opening = useRef20(false);
4082
+ const pointerFocus = useRef20(false);
4041
4083
  const floor = useTapFloor();
4042
4084
  const [width, setWidth] = useState16(0);
4043
4085
  const [openId, setOpenId] = useState16(null);
@@ -4058,7 +4100,7 @@ function Chart(props) {
4058
4100
  const boxHeight = Math.max(plotHeight, floor);
4059
4101
  const boxTop = -(boxHeight - plotHeight) / 2;
4060
4102
  const span = scale.max - scale.min || 1;
4061
- const y = useCallback5(
4103
+ const y = useCallback6(
4062
4104
  (value) => PAD + (scale.max - value) / span * (plotHeight - PAD * 2),
4063
4105
  [scale.max, span, plotHeight]
4064
4106
  );
@@ -4071,7 +4113,7 @@ function Chart(props) {
4071
4113
  cap * lanes + LANE_GAP * (lanes - 1)
4072
4114
  ));
4073
4115
  const barWidth = (groupWidth - LANE_GAP * (lanes - 1)) / lanes;
4074
- const centre = useCallback5((column) => column * colWidth + colWidth / 2, [colWidth]);
4116
+ const centre = useCallback6((column) => column * colWidth + colWidth / 2, [colWidth]);
4075
4117
  const marks = useMemo4(() => {
4076
4118
  const out = frame.bars.map((bar) => {
4077
4119
  let top = Math.min(y(bar.from), y(bar.to));
@@ -4145,9 +4187,9 @@ function Chart(props) {
4145
4187
  return [...out, ...dots];
4146
4188
  }, [frame, lines, keys, columns, centre, barWidth, groupWidth, y, floor, colWidth, plotWidth, boxTop, boxHeight]);
4147
4189
  const byId = useMemo4(() => new Map(marks.map((m) => [m.id, m])), [marks]);
4148
- const lead = useCallback5((column) => marks.find((m) => m.column === column), [marks]);
4190
+ const lead = useCallback6((column) => marks.find((m) => m.column === column), [marks]);
4149
4191
  const open = openId ? byId.get(openId) : void 0;
4150
- useEffect15(() => {
4192
+ useEffect16(() => {
4151
4193
  const el = plot.current;
4152
4194
  if (!el) return;
4153
4195
  const read = () => setWidth(el.clientWidth);
@@ -4157,20 +4199,20 @@ function Chart(props) {
4157
4199
  observer.observe(el);
4158
4200
  return () => observer.disconnect();
4159
4201
  }, []);
4160
- const syncEdges = useCallback5(() => {
4202
+ const syncEdges = useCallback6(() => {
4161
4203
  const el = scroller.current;
4162
4204
  if (!el) return;
4163
4205
  const last = el.scrollWidth - el.clientWidth;
4164
4206
  el.classList.toggle("is-more-before", el.scrollLeft > 1);
4165
4207
  el.classList.toggle("is-more-after", el.scrollLeft < last - 1);
4166
4208
  }, []);
4167
- useEffect15(syncEdges, [syncEdges, plotWidth, count]);
4209
+ useEffect16(syncEdges, [syncEdges, plotWidth, count]);
4168
4210
  useLayoutEffect5(() => {
4169
4211
  if (!openId || !frameEl.current || !tipEl.current || !svgEl.current) return;
4170
4212
  const anchor = [...svgEl.current.querySelectorAll("[data-anchor]")].find((el) => el.getAttribute("data-anchor") === openId);
4171
4213
  if (anchor) placeTip(frameEl.current, anchor, tipEl.current, frameEl.current);
4172
4214
  }, [openId, plotWidth, plotHeight]);
4173
- useEffect15(() => {
4215
+ useEffect16(() => {
4174
4216
  if (!openId) return;
4175
4217
  const doc = frameEl.current?.ownerDocument;
4176
4218
  const view = doc?.defaultView;
@@ -4198,7 +4240,7 @@ function Chart(props) {
4198
4240
  setOpenId(id);
4199
4241
  };
4200
4242
  const markAt = (target) => target.closest?.("[data-mark]")?.getAttribute("data-mark") ?? null;
4201
- const announce = useCallback5((column, picked = pick) => {
4243
+ const announce = useCallback6((column, picked = pick) => {
4202
4244
  const where = columns[column];
4203
4245
  if (!where) return;
4204
4246
  const values = marks.filter((m) => m.column === column).map((m) => `${m.name} ${format(m.value)}`).join(". ") || "No value";
@@ -2,7 +2,7 @@ import { button, esc } from './index.js';
2
2
  import { dropdown, wireDropdown } from './dropdown.js';
3
3
  import { filterChipText, filterChipName, filterChipUnset, filterChipItems, focusNextStop } from '../logic/filter-bar.js';
4
4
 
5
- export function filterBar({ filters = [], label = 'Filters', clearLabel = 'Clear all', disabled = false, busy = false } = {}) {
5
+ export function filterBar({ filters = [], label = 'Filters', clearLabel = 'Clear all', clearVariant = 'ghost', 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' : ''}>`
@@ -17,11 +17,10 @@ export function filterBar({ filters = [], label = 'Filters', clearLabel = 'Clear
17
17
  // row is the only thing in it, and it reads as a bar that has been turned
18
18
  // off rather than one with no filters on it. It is the kit's own `one-page`
19
19
  // rule — a control with no action to offer is not shown. It returns with
20
- // the first chip, in the bordered skin rather than the ghost one, because a
21
- // live action beside two chips has to read as live.
20
+ // the first chip, as a quiet text action.
22
21
  // why: docs/components.md#a-filter-row-holds-its-panels
23
22
  + (filters.length
24
- ? `<span data-filter-clear>${button({ label: clearLabel, size: 'sm', variant: 'ghost' })}</span>`
23
+ ? `<span data-filter-clear>${button({ label: clearLabel, size: 'sm', variant: clearVariant })}</span>`
25
24
  : '')
26
25
  + '</fieldset>';
27
26
  }
@@ -1 +1,9 @@
1
- export const calloutIcons = { info: 'info', success: 'check', warn: 'alert', danger: 'alert', neutral: 'info' };
1
+ // Default glyph per callout tone. A callout REPORTS a state, so its status
2
+ // glyphs are the circled ones — the same family toast() uses (TOAST_ICON in
3
+ // src/components/index.js) and the same split iconMeanings writes down. The
4
+ // bare `check` and `alert` this used to ship read as actions, which is why
5
+ // every showcase overrode them per instance. #453
6
+ //
7
+ // Neutral keeps `info` rather than the toast's `bolt`: a neutral callout is a
8
+ // standing note on the page, not something that just happened.
9
+ export const calloutIcons = { info: 'info', success: 'circleCheck', warn: 'circleAlert', danger: 'circleX', neutral: 'info' };
@@ -15,24 +15,29 @@
15
15
  padding: 3px 9px;
16
16
  border-radius: var(--radius-pill);
17
17
  color: var(--text);
18
- background: var(--surface); /* code-bg: inherit — decorative or text-only paint; nothing nested reads a ground from it. */
18
+ background: var(--chip-neutral-fill);
19
19
  white-space: nowrap;
20
20
  }
21
21
  /* Status chips take the --chip-* pairs, so the light app's solid fills live in
22
22
  tokens.css rather than in a theme override down here: on white the glow-tints
23
- wash out and the ink has to deepen to clear AA, and that is a token decision. */
24
- .ui-badge--live { color: var(--chip-success-ink); background: var(--chip-success-fill); /* code-bg: inherit — decorative or text-only paint; nothing nested reads a ground from it. */ }
25
- .ui-badge--soon { color: var(--accent); background: var(--surface); /* code-bg: inherit — a wash paints over a ground and behind a chip, so the opaque ground below it is the step. */ }
26
- .ui-badge--info { color: var(--chip-info-ink); background: var(--chip-info-fill); /* code-bg: inherit — decorative or text-only paint; nothing nested reads a ground from it. */ }
23
+ wash out and the ink has to deepen to clear AA, and that is a token decision.
24
+ The tones with no status take --chip-neutral-fill for the same reason: before #453
25
+ they painted the opaque --surface, the one soft badge fill that sat DARKER than the
26
+ panel in dark. why: docs/foundations.md#elevation
27
+ Soon takes body ink here, not --accent: accent ink measured 4.04:1 on a dark floating
28
+ panel under three of the four accents, body ink 9.74:1. See docs/foundations.md. */
29
+ .ui-badge--live { color: var(--chip-success-ink); background: var(--chip-success-fill); }
30
+ .ui-badge--soon { color: var(--text); background: var(--chip-neutral-fill); }
31
+ .ui-badge--info { color: var(--chip-info-ink); background: var(--chip-info-fill); }
27
32
  .ui-badge--warn { color: var(--chip-warn-ink); background: var(--chip-warn-fill); }
28
- .ui-badge--danger { color: var(--chip-danger-ink); background: var(--chip-danger-fill); /* code-bg: inherit — decorative or text-only paint; nothing nested reads a ground from it. */ }
29
- .ui-badge--archive { /* muted-ink: state — archived record */ color: var(--muted); background: var(--surface); /* code-bg: inherit — decorative or text-only paint; nothing nested reads a ground from it. */ }
33
+ .ui-badge--danger { color: var(--chip-danger-ink); background: var(--chip-danger-fill); }
34
+ .ui-badge--archive { /* muted-ink: state — archived record */ color: var(--muted); background: var(--chip-neutral-fill); }
30
35
 
31
36
  /* Semantic status variants — for record/row status in data tables
32
37
  (paid / verified / pending / failed …). Same chip, mapped to meaning. */
33
- .ui-badge--success { color: var(--chip-success-ink); background: var(--chip-success-fill); /* code-bg: inherit — decorative or text-only paint; nothing nested reads a ground from it. */ }
38
+ .ui-badge--success { color: var(--chip-success-ink); background: var(--chip-success-fill); }
34
39
  .ui-badge--pending { color: var(--chip-warn-ink); background: var(--chip-warn-fill); }
35
- .ui-badge--neutral { color: var(--text); background: var(--surface); /* code-bg: inherit — decorative or text-only paint; nothing nested reads a ground from it. */ }
40
+ .ui-badge--neutral { color: var(--text); background: var(--chip-neutral-fill); }
36
41
 
37
42
  /* Pill: same shape, sentence case, a touch larger — a metadata chip */
38
43
  .ui-pill {
@@ -42,7 +47,7 @@
42
47
  /* rank: chip */
43
48
  font-size: var(--text-xs);
44
49
  color: var(--text);
45
- background: var(--surface); /* code-bg: inherit — decorative or text-only paint; nothing nested reads a ground from it. */
50
+ background: var(--chip-neutral-fill);
46
51
  border-radius: var(--radius-pill);
47
52
  padding: 4px 11px;
48
53
  font-weight: var(--weight-semibold);
@@ -50,8 +55,8 @@
50
55
  /* The live pill is the same status paint as the live badge, so it takes the
51
56
  same pair. On its own it was --green on --glow-green, which is 3.97:1 over
52
57
  white — under AA, while the badge beside it cleared at 4.64:1. See issue #131. */
53
- .ui-pill--live { color: var(--chip-success-ink); background: var(--chip-success-fill); /* code-bg: inherit — decorative or text-only paint; nothing nested reads a ground from it. */ }
54
- .ui-pill--soon { color: var(--accent); background: var(--surface); /* code-bg: inherit — a wash paints over a ground and behind a chip, so the opaque ground below it is the step. */ }
58
+ .ui-pill--live { color: var(--chip-success-ink); background: var(--chip-success-fill); }
59
+ .ui-pill--soon { color: var(--text); background: var(--chip-neutral-fill); }
55
60
 
56
61
  /* Status dot */
57
62
  .ui-dot {
@@ -1,5 +1,5 @@
1
1
  /* ============================================================================
2
- * Button — .ui-btn (variants: primary / secondary / ghost / danger)
2
+ * Button — .ui-btn (variants: primary / secondary / ghost / danger / link)
3
3
  * (sizes: xs / sm / md / lg, [icon], [disabled], [aria-busy])
4
4
  * ========================================================================== */
5
5
 
@@ -70,6 +70,22 @@
70
70
  color: var(--strong);
71
71
  }
72
72
 
73
+ .ui-btn--link {
74
+ background: transparent;
75
+ border-color: transparent;
76
+ color: var(--accent);
77
+ padding-inline: 0;
78
+ }
79
+ .ui-btn--link .ui-btn__label-slot { overflow: visible; }
80
+ .ui-btn--link[aria-busy="true"] .ui-btn__label-slot { overflow: hidden; }
81
+ .ui-btn--link .ui-btn__label { text-decoration: underline; text-underline-offset: 0.2em; }
82
+ .ui-btn.ui-btn--link:hover {
83
+ background: transparent;
84
+ border-color: transparent;
85
+ color: var(--strong);
86
+ text-decoration: underline;
87
+ }
88
+
73
89
  .ui-btn--danger { color: var(--text); }
74
90
  .ui-btn--danger:hover, .ui-btn--danger:focus-visible {
75
91
  color: var(--pink);
@@ -112,6 +128,12 @@
112
128
  border-color: transparent;
113
129
  /* muted-ink: state — unavailable control */ color: var(--disabled-ink-bare);
114
130
  }
131
+ .ui-btn--link:disabled,
132
+ .ui-btn--link[aria-disabled="true"] {
133
+ background: transparent;
134
+ border-color: transparent;
135
+ /* muted-ink: state — unavailable link action */ color: var(--disabled-ink-bare);
136
+ }
115
137
  /* With no box, the ink is read on whatever is behind it, so the ghost takes an ink
116
138
  set for the dullest ground: in dark 7.49 / 6.31 / 7.00 / 5.44 on --bg / --surface /
117
139
  --surface-2 / --surface-3, in light 5.85 / 6.50 / 5.49 / 5.70. The enabled ghost now uses body
@@ -161,6 +183,13 @@
161
183
  border-color: transparent;
162
184
  color: var(--text);
163
185
  }
186
+ .ui-btn[aria-busy="true"].ui-btn--link,
187
+ .ui-btn[aria-busy="true"].ui-btn--link:hover,
188
+ .ui-btn[aria-busy="true"].ui-btn--link:active {
189
+ background: transparent; /* code-bg: inherit — busy paint retains the button's containing gap. */
190
+ border-color: transparent;
191
+ color: var(--accent);
192
+ }
164
193
  .ui-btn[aria-busy="true"].ui-btn--danger,
165
194
  .ui-btn[aria-busy="true"].ui-btn--danger:hover,
166
195
  .ui-btn[aria-busy="true"].ui-btn--danger:active {
@@ -40,6 +40,11 @@
40
40
  .ui-card--pad-sm { padding: var(--space-5) var(--space-6); }
41
41
  .ui-card--pad-lg { padding: var(--space-8) var(--space-10); }
42
42
 
43
+ /* A card framing one content-width block ends where the block ends — a 359px table sat in
44
+ a 1232px card (#504) — and is capped, so a wider block still fills the room and scrolls.
45
+ why: docs/foundations.md#spacing-and-rhythm */
46
+ .ui-card--fit { width: fit-content; max-width: 100%; }
47
+
43
48
  /* An accent-tinted card, e.g. a highlighted plan / success surface. The accent edge is
44
49
  the card's own hairline re-coloured: an inset ring over it would draw two. #295
45
50
  The strength is --card-tint, per theme: light mixes down from white, so a tint that
@@ -295,12 +295,12 @@
295
295
  margin-left: auto;
296
296
  flex: none;
297
297
  color: var(--text);
298
- background: color-mix(in srgb, var(--muted) 12%, transparent); /* code-bg: inherit — decorative or text-only paint; nothing nested reads a ground from it. */
299
- }
300
- .ui-dropdown__badge.is-live { color: var(--chip-success-ink); background: var(--chip-success-fill); /* code-bg: inherit — decorative or text-only paint; nothing nested reads a ground from it. */ }
301
- :root[data-theme="light"] .ui-dropdown__badge:not(.is-live) {
302
- background: color-mix(in srgb, var(--muted) 15%, transparent); /* code-bg: inherit — decorative or text-only paint; nothing nested reads a ground from it. */
298
+ /* The kit's one neutral chip fill, which #453 moved into a token so the badge, the
299
+ pill, this chip and the version badge are one family. The per-theme alphas that
300
+ used to be written here twice live with the token. */
301
+ background: var(--chip-neutral-fill); /* ring-gap: inherit — translucent wash retains the opaque containing gap. */
303
302
  }
303
+ .ui-dropdown__badge.is-live { color: var(--chip-success-ink); background: var(--chip-success-fill); /* code-bg: inherit — decorative or text-only surface has no nested chip. */ }
304
304
 
305
305
  /* Disabled + danger rows */
306
306
  .ui-dropdown__item.is-disabled { /* muted-ink: state — unavailable control */ color: var(--disabled-ink); cursor: not-allowed; pointer-events: none; }
@@ -10,6 +10,7 @@
10
10
  .ui-pager {
11
11
  display: flex;
12
12
  flex-wrap: wrap;
13
+ justify-content: flex-end;
13
14
  align-items: center;
14
15
  gap: var(--space-3) var(--space-4);
15
16
  font-size: var(--text-sm);
@@ -113,7 +114,8 @@
113
114
  `margin-top: 16px` for this, which is --space-4; the number moves to the scale
114
115
  with it rather than travelling as a literal.
115
116
  why: docs/foundations.md#spacing-and-rhythm */
116
- .ui-table + .ui-pager { margin-top: var(--space-4); }
117
+ .ui-table + .ui-pager,
118
+ .ui-table-scroll + .ui-pager { margin-top: var(--space-4); }
117
119
 
118
120
  /* A step given an `href` is an anchor, and a host stylesheet's `a:link` is
119
121
  (0,1,1) — which outranks `.ui-btn--ghost` at (0,1,0) and paints the enabled
@@ -8,6 +8,11 @@
8
8
  border-collapse: collapse;
9
9
  font-size: var(--text-base);
10
10
  }
11
+ /* Fit cards keep short tables close to their labels. Other cards keep full-width tables. */
12
+ :where(.ui-card--fit) .ui-table {
13
+ width: auto;
14
+ max-width: 100%;
15
+ }
11
16
  .ui-table th {
12
17
  text-align: left;
13
18
  /* rank: label */
@@ -280,6 +285,10 @@
280
285
  width: calc(100% + 2 * var(--space-3));
281
286
  margin-inline: calc(-1 * var(--space-3));
282
287
  }
288
+ .ui-card--fit > :where(.ui-table--dense, .ui-table--zebra) {
289
+ width: auto;
290
+ max-width: calc(100% + 2 * var(--space-3));
291
+ }
283
292
  /* A scroll wrapper owns the bleed for a nested dense table: 4px of focus
284
293
  clearance plus the 12px end-cell inset. */
285
294
  .ui-card > .ui-table-scroll:has(> .ui-table--dense) {
@@ -391,6 +400,8 @@ a.ui-identity:hover { text-decoration: underline; }
391
400
  under it, so whatever the cut lets out is painted over that column. A child's ring
392
401
  needs 3px and the cell's padding is 12px, so the room comes from the padding.
393
402
  why: docs/components.md#dense-financial-tables, #500, #513 */
403
+ /* A percentage can stretch an intrinsic table beyond its content. */
404
+ .ui-card--fit .ui-table--pinned .ui-table__identity { width: 1px; }
394
405
  .ui-table--pinned .ui-table__identity {
395
406
  width: 1%;
396
407
  max-width: var(--ui-table-identity-max);
@@ -226,8 +226,11 @@
226
226
  flex: none;
227
227
  margin-top: 1px;
228
228
  }
229
- .vbadge.live { color: var(--chip-success-ink); background: var(--chip-success-fill); /* code-bg: inherit — decorative or text-only paint; nothing nested reads a ground from it. */ }
230
- .vbadge.arch { /* muted-ink: state — archived version */ color: var(--muted); background: var(--surface); /* code-bg: inherit — decorative or text-only paint; nothing nested reads a ground from it. */ }
229
+ .vbadge.live { color: var(--chip-success-ink); background: var(--chip-success-fill); /* code-bg: inherit — decorative or text-only surface has no nested chip. */ }
230
+ /* The archived version badge is the archive badge tone in a floating menu, so it takes
231
+ the same neutral chip fill rather than the card surface — which, inside a panel that
232
+ is itself lighter than a card, was darker than the ground it sat on. #453 */
233
+ .vbadge.arch { /* muted-ink: state — archived version */ color: var(--muted); background: var(--chip-neutral-fill); /* ring-gap: inherit — translucent wash retains the opaque containing gap. */ }
231
234
  /* The phone step — the same one the rail folds at: docs/foundations.md#breakpoints.
232
235
  The switcher keeps its glyph and drops its label.
233
236
  The menu hangs the other way here. Anchored by its left edge it opened
@@ -233,10 +233,18 @@
233
233
  --chip-success-fill: var(--glow-green);
234
234
  --chip-warn-ink: var(--amber);
235
235
  --chip-warn-fill: color-mix(in srgb, var(--amber) 15%, transparent);
236
- --chip-danger-ink: var(--pink);
236
+ /* Not --pink: that reads 4.23:1 on this fill over a floating panel, under the 4.5:1 badge
237
+ floor. Same hue lifted toward white — 8.64 page / 7.02 card / 6.30 panel. The signal
238
+ and the solid-toast pair keep --pink. why: docs/foundations.md#colour-and-contrast, #453 */
239
+ --chip-danger-ink: #f5aec6;
237
240
  --chip-danger-fill: var(--glow-pink);
238
241
  --chip-info-ink: var(--cyan);
239
242
  --chip-info-fill: var(--glow-cyan);
243
+ /* The fill of a chip with no status to carry. The themes match on SEPARATION, not on
244
+ alpha — 12% here is 1.22:1 off the card, 15% in light is 1.23:1 — because --muted is
245
+ a light violet-grey in dark and a mid slate in light.
246
+ why: docs/foundations.md#colour-and-contrast, #453 */
247
+ --chip-neutral-fill: color-mix(in srgb, var(--muted) 12%, transparent);
240
248
 
241
249
  /* Solid signal surfaces — a status that has stopped being ink and become the FILL (a
242
250
  solid toast). Fill and ink are ONE pair, never two independent picks: the fill is the
@@ -412,7 +420,10 @@
412
420
  chip text clears WCAG AA on its own pale fill, and on the soft toast's wash, which
413
421
  #131 gave the same ink. Three came down again at #295 with the page; danger did not
414
422
  need to. */
415
- --chip-success-ink: #1c7034;
423
+ /* Success is held at the 7:1 body aim rather than the 4.5:1 floor: 7.17:1 on its own
424
+ fill, 8.33:1 on the card. Was #1c7034 at 5.29:1. The fill did not move.
425
+ why: docs/foundations.md#colour-and-contrast, #453 */
426
+ --chip-success-ink: #145a26;
416
427
  --chip-success-fill: #dff3e4;
417
428
  --chip-warn-ink: #825900;
418
429
  --chip-warn-fill: #fbedd2;
@@ -423,6 +434,8 @@
423
434
  already occupy. See issue #131. */
424
435
  --chip-info-ink: #096a7c;
425
436
  --chip-info-fill: #ddeff3;
437
+ /* See the dark block for why the two alphas differ. */
438
+ --chip-neutral-fill: color-mix(in srgb, var(--muted) 13%, transparent);
426
439
 
427
440
  /* Light runs the same rule the other way. The signals are mid-tone here, so NO ink
428
441
  rescues them and the fill is what has to move: it takes the deepened form the chip
@@ -579,3 +592,18 @@
579
592
  .ui-callout--warn,
580
593
  .ui-callout--danger,
581
594
  .ui-success { --code-bg: var(--wash-code-bg); }
595
+
596
+ /* React badges and pills accept inline code on opaque or translucent status fills. */
597
+ .ui-badge,
598
+ .ui-badge--live,
599
+ .ui-badge--soon,
600
+ .ui-badge--info,
601
+ .ui-badge--warn,
602
+ .ui-badge--danger,
603
+ .ui-badge--archive,
604
+ .ui-badge--success,
605
+ .ui-badge--pending,
606
+ .ui-badge--neutral,
607
+ .ui-pill,
608
+ .ui-pill--live,
609
+ .ui-pill--soon { --code-bg: var(--wash-code-bg); }