@godxjp/ui 32.7.1 → 32.7.2

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.
@@ -3,7 +3,7 @@
3
3
  You are about to write code against a design system you did not author. This file is the whole
4
4
  contract. Read it before you write JSX.
5
5
 
6
- **This catalog describes `@godxjp/ui` 32.7.1.** If the project you are editing has a different
6
+ **This catalog describes `@godxjp/ui` 32.7.2.** If the project you are editing has a different
7
7
  version in its `package.json`, read the pinned catalog for THAT version instead
8
8
  (`…/v<their-version>/agent/…`). A catalog newer than the installed package describes props that do
9
9
  not exist yet; older, and it hides props that do. Neither failure announces itself.
@@ -73,6 +73,7 @@
73
73
  "storyPath": "data-display/list-row.tsx",
74
74
  "tagline": "Single-line entity row (leading · title/description · trailing action) for SHORT lists inside a Card — sessions, API tokens, linked accounts, passkeys, MFA factors, invitations.",
75
75
  "usage": [
76
+ "DO rely on ListRow for a cut title (gh#1298): a title or description that is actually truncated gets a `title` with its text (a `Text ellipsis` inside it, never a badge beside it) and unfolds while the row holds keyboard focus; when nothing in the row takes focus, the cut line gets its own tab stop. Do not add a hand-rolled Tooltip for that.",
76
77
  "DO use ListRow for a SHORT (≈2–8 item) list of entities inside a Card where each row is one line with an action — account sessions, API keys, linked identities, passkeys. Stack rows in a `<Card><CardContent flush>` so the rows draw their own quiet dividers edge-to-edge.",
77
78
  "DON'T reach for DataTable here — it carries sorting/selection/pagination chrome that a 3-item list doesn't need. DON'T nest a Card per row either (card-in-card). ListRow is the in-between surface.",
78
79
  "DO host ListRow in a DataTable column when the list needs ROW SELECTION (a mail/notification inbox with bulk actions) — DataTable is the only primitive that selects rows. Mark that column `flush: true` so the cell drops its own padding and the ListRow owns the inset (gh#1016); without it the cell padding stacks on the row's (a 32px dead gap after the checkbox, taller rows) and the unread band stops at the checkbox cell. DON'T zero the cell with a className.",
@@ -4352,6 +4352,7 @@
4352
4352
  "storyPath": "data-display/list-row.tsx",
4353
4353
  "tagline": "Single-line entity row (leading · title/description · trailing action) for SHORT lists inside a Card — sessions, API tokens, linked accounts, passkeys, MFA factors, invitations.",
4354
4354
  "usage": [
4355
+ "DO rely on ListRow for a cut title (gh#1298): a title or description that is actually truncated gets a `title` with its text (a `Text ellipsis` inside it, never a badge beside it) and unfolds while the row holds keyboard focus; when nothing in the row takes focus, the cut line gets its own tab stop. Do not add a hand-rolled Tooltip for that.",
4355
4356
  "DO use ListRow for a SHORT (≈2–8 item) list of entities inside a Card where each row is one line with an action — account sessions, API keys, linked identities, passkeys. Stack rows in a `<Card><CardContent flush>` so the rows draw their own quiet dividers edge-to-edge.",
4356
4357
  "DON'T reach for DataTable here — it carries sorting/selection/pagination chrome that a 3-item list doesn't need. DON'T nest a Card per row either (card-in-card). ListRow is the in-between surface.",
4357
4358
  "DO host ListRow in a DataTable column when the list needs ROW SELECTION (a mail/notification inbox with bulk actions) — DataTable is the only primitive that selects rows. Mark that column `flush: true` so the cell drops its own padding and the ListRow owns the inset (gh#1016); without it the cell padding stacks on the row's (a 32px dead gap after the checkbox, taller rows) and the unread band stops at the checkbox cell. DON'T zero the cell with a className.",
package/agent/index.json CHANGED
@@ -54,7 +54,7 @@
54
54
  "note": "Pin to the tag that matches the @godxjp/ui version you installed. A catalog newer than your package describes props you do not have; older, and it hides props you do.",
55
55
  "read": {
56
56
  "live": "https://raw.githubusercontent.com/godx-jp/godxjp-ui/main/agent/index.json",
57
- "pinned": "https://raw.githubusercontent.com/godx-jp/godxjp-ui/v32.7.1/agent/index.json"
57
+ "pinned": "https://raw.githubusercontent.com/godx-jp/godxjp-ui/v32.7.2/agent/index.json"
58
58
  },
59
59
  "source": "mcp/src/data — the same data @godxjp/ui-mcp serves — plus the foundation and semantic token tiers, read from src/tokens/*.css",
60
60
  "start": "https://raw.githubusercontent.com/godx-jp/godxjp-ui/main/agent/START-HERE.md",
@@ -68,5 +68,5 @@
68
68
  "foundation": "the seeds a consumer is invited to set — --primary, --background, --radius",
69
69
  "semantic": "named roles that follow the seeds — --ring, --text-link, --overlay-background"
70
70
  },
71
- "version": "32.7.1"
71
+ "version": "32.7.2"
72
72
  }
package/agent/llms.txt CHANGED
@@ -1,10 +1,10 @@
1
1
  # @godxjp/ui
2
2
 
3
3
  > A Japanese-enterprise React design system: 161 components, 2197 design tokens,
4
- > 43 cardinal rules. This file is the entry point for AI agents. Catalog version 32.7.1.
4
+ > 43 cardinal rules. This file is the entry point for AI agents. Catalog version 32.7.2.
5
5
 
6
6
  If your client can run a process, do not read these files — run the MCP server instead
7
- (`npx @godxjp/ui-mcp@32.7.1`). It is searchable and version-locked. These files exist for agents
7
+ (`npx @godxjp/ui-mcp@32.7.2`). It is searchable and version-locked. These files exist for agents
8
8
  that can only fetch URLs.
9
9
 
10
10
  ## Start
@@ -27,7 +27,7 @@ that can only fetch URLs.
27
27
  ## Pinning
28
28
 
29
29
  Every URL above tracks `main`. To pin to the release a project actually installed, swap `main` for
30
- the tag: `.../godx-jp/godxjp-ui/v32.7.1/agent/...`. A catalog that does not match the installed
30
+ the tag: `.../godx-jp/godxjp-ui/v32.7.2/agent/...`. A catalog that does not match the installed
31
31
  package describes props that are absent, or hides props that are present, and says nothing either way.
32
32
 
33
33
  Pinned catalogs only exist for releases whose tag actually contains `agent/`. If `…/v<version>/agent/index.json` returns 404, that release predates this catalog: read `…/main/…` instead and compare `index.json` → `version` against the package you have, so you at least know which way it drifted.
@@ -71,6 +71,7 @@ import {
71
71
  import { cn } from "../../lib/utils.js";
72
72
  import { useScrollsHorizontally } from "../../lib/hooks.js";
73
73
  import { densityClass } from "../../lib/variants.js";
74
+ import { TRUNCATION_FOCUSABLE, truncatedPart } from "../../lib/truncation.js";
74
75
  import {
75
76
  controlIconSmClass,
76
77
  tableCellPaddingClass,
@@ -728,16 +729,6 @@ DataTable.DensityToggle.displayName = "DataTable.DensityToggle";
728
729
  function fixedEdge(col) {
729
730
  return col.fixed ?? (col.pin === "end" ? "end" : void 0);
730
731
  }
731
- const TRUNCATED_CELL_FOCUSABLE = 'a[href], button, input, select, textarea, [tabindex]:not([tabindex="-1"]), [contenteditable="true"]';
732
- function truncatedPart(cell) {
733
- if (cell.scrollWidth - cell.clientWidth > 1) return cell;
734
- for (const el of cell.querySelectorAll("*")) {
735
- if (el.scrollWidth - el.clientWidth > 1 && getComputedStyle(el).textOverflow === "ellipsis") {
736
- return el;
737
- }
738
- }
739
- return null;
740
- }
741
732
  function sameOffsets(a, b) {
742
733
  const keys = Object.keys(a);
743
734
  if (keys.length !== Object.keys(b).length) return false;
@@ -934,7 +925,7 @@ DataTable.Content = function DataTableContent() {
934
925
  cell.title = (part.textContent ?? "").replace(/\s+/g, " ").trim();
935
926
  cell.dataset.truncatedTitle = "";
936
927
  }
937
- if (!cell.querySelector(TRUNCATED_CELL_FOCUSABLE)) cell.tabIndex = 0;
928
+ if (!cell.querySelector(TRUNCATION_FOCUSABLE)) cell.tabIndex = 0;
938
929
  } else if (cell.dataset.truncated !== void 0) {
939
930
  delete cell.dataset.truncated;
940
931
  if (ownTitle) {
@@ -4,7 +4,56 @@ import * as React from "react";
4
4
  import { Slot } from "../../lib/slot.js";
5
5
  import { useTranslation } from "../../i18n/use-translation.js";
6
6
  import { cn } from "../../lib/utils.js";
7
+ import { TRUNCATION_FOCUSABLE, truncatedPart } from "../../lib/truncation.js";
7
8
  import { Text } from "../general/typography.js";
9
+ function useTruncatedLines(rowRef, enabled) {
10
+ const mark = React.useCallback(() => {
11
+ const row = rowRef.current;
12
+ if (!row) return;
13
+ const rowFocusable = row.matches(TRUNCATION_FOCUSABLE) || row.querySelector(TRUNCATION_FOCUSABLE) !== null;
14
+ for (const line of row.querySelectorAll('[data-slot="list-row-body"] > *')) {
15
+ if (row.contains(document.activeElement) && line.dataset.truncated !== void 0) continue;
16
+ const ownTitle = line.dataset.truncatedTitle !== void 0;
17
+ const part = truncatedPart(line);
18
+ if (part) {
19
+ line.dataset.truncated = "";
20
+ if ((!line.title || ownTitle) && (part === line || !part.title)) {
21
+ line.title = (part.textContent ?? "").replace(/\s+/g, " ").trim();
22
+ line.dataset.truncatedTitle = "";
23
+ }
24
+ if (!rowFocusable || line.dataset.truncatedStop !== void 0) {
25
+ line.tabIndex = 0;
26
+ line.dataset.truncatedStop = "";
27
+ }
28
+ } else if (line.dataset.truncated !== void 0) {
29
+ delete line.dataset.truncated;
30
+ if (ownTitle) {
31
+ line.removeAttribute("title");
32
+ delete line.dataset.truncatedTitle;
33
+ }
34
+ if (line.dataset.truncatedStop !== void 0) {
35
+ line.removeAttribute("tabindex");
36
+ delete line.dataset.truncatedStop;
37
+ }
38
+ }
39
+ }
40
+ }, [rowRef]);
41
+ React.useEffect(() => {
42
+ if (enabled) mark();
43
+ });
44
+ React.useEffect(() => {
45
+ const row = rowRef.current;
46
+ if (!enabled || !row || typeof ResizeObserver === "undefined") return;
47
+ const observer = new ResizeObserver(mark);
48
+ observer.observe(row);
49
+ const refold = () => requestAnimationFrame(mark);
50
+ row.addEventListener("focusout", refold);
51
+ return () => {
52
+ observer.disconnect();
53
+ row.removeEventListener("focusout", refold);
54
+ };
55
+ }, [enabled, mark, rowRef]);
56
+ }
8
57
  const ListRow = React.forwardRef(
9
58
  ({
10
59
  as = "div",
@@ -22,6 +71,16 @@ const ListRow = React.forwardRef(
22
71
  ...props
23
72
  }, ref) => {
24
73
  const { t } = useTranslation();
74
+ const rowRef = React.useRef(null);
75
+ const setRef = React.useCallback(
76
+ (node) => {
77
+ rowRef.current = node;
78
+ if (typeof ref === "function") ref(node);
79
+ else if (ref) ref.current = node;
80
+ },
81
+ [ref]
82
+ );
83
+ useTruncatedLines(rowRef, overflow !== "wrap");
25
84
  const item = asChild && as !== "div" ? as : null;
26
85
  const Comp = asChild ? Slot : as;
27
86
  const truncate = overflow !== "wrap";
@@ -37,7 +96,7 @@ const ListRow = React.forwardRef(
37
96
  const row = /* @__PURE__ */ jsx(
38
97
  Comp,
39
98
  {
40
- ref,
99
+ ref: setRef,
41
100
  "data-slot": "list-row",
42
101
  "data-align": align === "start" ? "start" : void 0,
43
102
  "data-overflow": overflow === "wrap" ? "wrap" : void 0,
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "$comment": "AUTO-GENERATED by scripts/gen-measurement-contract.mjs — do not edit. Read this instead of guessing: docs/MEASUREMENT-CONTRACT.md.",
3
- "version": "32.7.1",
3
+ "version": "32.7.2",
4
4
  "targetSize": {
5
5
  "standard": "WCAG 2.2 SC 2.5.8 Target Size (Minimum), level AA — 24×24 CSS px",
6
6
  "min": 24,
@@ -0,0 +1,14 @@
1
+ /**
2
+ * What is actually CUT inside a truncating box, shared by DataTable `ellipsis` cells (gh#1262,
3
+ * gh#1269) and ListRow titles (gh#1298).
4
+ *
5
+ * Internal: reachable from no published subpath.
6
+ */
7
+ /** Something that already takes focus — a box holding one needs no tab stop of its own. */
8
+ export declare const TRUNCATION_FOCUSABLE = "a[href], button, input, select, textarea, [tabindex]:not([tabindex=\"-1\"]), [contenteditable=\"true\"]";
9
+ /**
10
+ * The box itself when it overflows, else the first descendant that ends in an ellipsis
11
+ * (`text-overflow: ellipsis`) and overflows — a `Text truncate` link beside a badge or an avatar
12
+ * is cut while its container fits. `null` when nothing is cut.
13
+ */
14
+ export declare function truncatedPart(box: HTMLElement): HTMLElement | null;
@@ -0,0 +1,14 @@
1
+ const TRUNCATION_FOCUSABLE = 'a[href], button, input, select, textarea, [tabindex]:not([tabindex="-1"]), [contenteditable="true"]';
2
+ function truncatedPart(box) {
3
+ if (box.scrollWidth - box.clientWidth > 1) return box;
4
+ for (const el of box.querySelectorAll("*")) {
5
+ if (el.scrollWidth - el.clientWidth > 1 && getComputedStyle(el).textOverflow === "ellipsis") {
6
+ return el;
7
+ }
8
+ }
9
+ return null;
10
+ }
11
+ export {
12
+ TRUNCATION_FOCUSABLE,
13
+ truncatedPart
14
+ };
@@ -912,6 +912,18 @@
912
912
  > [data-slot="list-row"] {
913
913
  padding-inline: 0;
914
914
  }
915
+
916
+ [data-slot="list-row"]:is(:focus-visible, :has(:focus-visible))
917
+ [data-slot="list-row-body"]
918
+ > [data-truncated],
919
+ [data-slot="list-row"]:is(:focus-visible, :has(:focus-visible))
920
+ [data-slot="list-row-body"]
921
+ > [data-truncated]
922
+ [data-truncate] {
923
+ overflow: visible;
924
+ white-space: normal;
925
+ overflow-wrap: anywhere;
926
+ }
915
927
  [data-slot="list-row"][data-density="compact"] [data-slot="list-row-body"] {
916
928
  min-inline-size: min(var(--list-row-compact-body-min-width, 6rem), 100%);
917
929
  flex-basis: var(--list-row-compact-body-min-width, 6rem);
@@ -7,6 +7,7 @@
7
7
  [data-interactive] > [data-slot="table-cell"] a[href],
8
8
  [data-interactive] > [data-slot="table-cell"] button,
9
9
  .ui-data-table-ellipsis[data-truncated],
10
+ [data-slot="list-row"] [data-truncated][tabindex],
10
11
  .ui-input,
11
12
  .ui-control,
12
13
  .ui-control-multiline,
@@ -1,5 +1,5 @@
1
1
  {
2
- "version": "32.7.1",
2
+ "version": "32.7.2",
3
3
  "generatedBy": "scripts/gen-style-layers.mjs — do not edit by hand; run `pnpm gen:style-layers`",
4
4
  "base": "base.css",
5
5
  "fonts": "fonts.css",