@godxjp/ui 27.5.0 → 27.7.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.
@@ -6,6 +6,12 @@ export type RangeTimelineRow = {
6
6
  end: number;
7
7
  startLabel: string;
8
8
  endLabel: string;
9
+ /**
10
+ * Nesting level, 0 for a top-level row. Rows arrive flat and depth-first (a parent, then its
11
+ * descendants); a row is a parent when the row after it is deeper. The component draws the
12
+ * indent (`--range-timeline-indent-width` per level) and the disclosure.
13
+ */
14
+ depth?: number;
9
15
  };
10
16
  export type RangeTimelineProps = React.HTMLAttributes<HTMLElement> & {
11
17
  label: string;
@@ -28,6 +34,15 @@ export type RangeTimelineProps = React.HTMLAttributes<HTMLElement> & {
28
34
  * the header-only ruling. Colour `--range-timeline-grid-color`, weight `--range-timeline-grid-width`.
29
35
  */
30
36
  bordered?: boolean;
37
+ /**
38
+ * Controlled ids of the EXPANDED parent rows (same spelling as `Tree expandedValues`). A folded
39
+ * parent hides every descendant row — label and bar.
40
+ */
41
+ expandedValues?: readonly string[];
42
+ /** Uncontrolled initial expanded parents. Omitted: every parent starts expanded. */
43
+ defaultExpandedValues?: readonly string[];
44
+ /** Fires with the next expanded parent ids, for controlled and uncontrolled timelines alike. */
45
+ onExpandedValuesChange?: (values: string[]) => void;
31
46
  };
32
47
  /** A horizontal range axis. Units and labels are data; the design system owns all geometry. */
33
48
  export declare const RangeTimeline: React.ForwardRefExoticComponent<React.HTMLAttributes<HTMLElement> & {
@@ -51,4 +66,13 @@ export declare const RangeTimeline: React.ForwardRefExoticComponent<React.HTMLAt
51
66
  * the header-only ruling. Colour `--range-timeline-grid-color`, weight `--range-timeline-grid-width`.
52
67
  */
53
68
  bordered?: boolean;
69
+ /**
70
+ * Controlled ids of the EXPANDED parent rows (same spelling as `Tree expandedValues`). A folded
71
+ * parent hides every descendant row — label and bar.
72
+ */
73
+ expandedValues?: readonly string[];
74
+ /** Uncontrolled initial expanded parents. Omitted: every parent starts expanded. */
75
+ defaultExpandedValues?: readonly string[];
76
+ /** Fires with the next expanded parent ids, for controlled and uncontrolled timelines alike. */
77
+ onExpandedValuesChange?: (values: string[]) => void;
54
78
  } & React.RefAttributes<HTMLElement>>;
@@ -2,13 +2,67 @@
2
2
  import { jsx, jsxs } from "react/jsx-runtime";
3
3
  import * as React from "react";
4
4
  import { useTranslation } from "../../i18n/use-translation.js";
5
- import { ChevronLeft, ChevronRight, GripVertical } from "lucide-react";
5
+ import { ChevronDown, ChevronLeft, ChevronRight, GripVertical } from "lucide-react";
6
6
  import { Button } from "../general/button.js";
7
7
  import { Text } from "../general/typography.js";
8
8
  import { cn } from "../../lib/utils.js";
9
9
  const RangeTimeline = React.forwardRef(
10
- function RangeTimeline2({ label, columns, bands, rows, today, onRangeChange, bordered = true, className, ...props }, ref) {
10
+ function RangeTimeline2({
11
+ label,
12
+ columns,
13
+ bands,
14
+ rows,
15
+ today,
16
+ onRangeChange,
17
+ bordered = true,
18
+ expandedValues,
19
+ defaultExpandedValues,
20
+ onExpandedValuesChange,
21
+ className,
22
+ ...props
23
+ }, ref) {
11
24
  const { t } = useTranslation();
25
+ const reactId = React.useId();
26
+ const depthOf = (row) => Math.max(0, Math.floor(row.depth ?? 0));
27
+ const nested = rows.some((row) => depthOf(row) > 0);
28
+ const parents = rows.filter((row, index) => index + 1 < rows.length && depthOf(rows[index + 1]) > depthOf(row)).map((row) => row.id);
29
+ const [collapsed, setCollapsed] = React.useState(
30
+ () => new Set(
31
+ defaultExpandedValues ? parents.filter((id) => !defaultExpandedValues.includes(id)) : []
32
+ )
33
+ );
34
+ const isExpanded = (id) => expandedValues ? expandedValues.includes(id) : !collapsed.has(id);
35
+ const toggleRow = (id) => {
36
+ const open = !isExpanded(id);
37
+ if (!expandedValues) {
38
+ const next = new Set(collapsed);
39
+ if (open) next.delete(id);
40
+ else next.add(id);
41
+ setCollapsed(next);
42
+ }
43
+ onExpandedValuesChange?.(
44
+ parents.filter((parent) => parent === id ? open : isExpanded(parent))
45
+ );
46
+ };
47
+ let foldedAt = null;
48
+ const visibleRows = rows.filter((row) => {
49
+ const depth = depthOf(row);
50
+ if (foldedAt !== null && depth > foldedAt) return false;
51
+ foldedAt = parents.includes(row.id) && !isExpanded(row.id) ? depth : null;
52
+ return true;
53
+ });
54
+ const siblingsOf = (index) => {
55
+ const depth = depthOf(visibleRows[index]);
56
+ let first = index;
57
+ while (first > 0 && depthOf(visibleRows[first - 1]) >= depth) first -= 1;
58
+ let size = 0, position = 0;
59
+ for (let i = first; i < visibleRows.length && depthOf(visibleRows[i]) >= depth; i += 1) {
60
+ if (depthOf(visibleRows[i]) !== depth) continue;
61
+ size += 1;
62
+ if (i === index) position = size;
63
+ }
64
+ return { size, position };
65
+ };
12
66
  const units = Math.max(
13
67
  1,
14
68
  columns.reduce((sum, column) => sum + column.units, 0)
@@ -44,7 +98,10 @@ const RangeTimeline = React.forwardRef(
44
98
  "div",
45
99
  {
46
100
  className: "ui-range-timeline-canvas",
47
- style: { "--range-timeline-columns": Math.max(1, columns.length) },
101
+ style: {
102
+ "--range-timeline-columns": Math.max(1, columns.length),
103
+ "--range-timeline-units": units
104
+ },
48
105
  children: [
49
106
  bands && bands.length > 0 && /* @__PURE__ */ jsxs("div", { className: "ui-range-timeline-header", children: [
50
107
  /* @__PURE__ */ jsx("div", { className: "ui-range-timeline-label", "aria-hidden": "true" }),
@@ -52,7 +109,7 @@ const RangeTimeline = React.forwardRef(
52
109
  "div",
53
110
  {
54
111
  className: "ui-range-timeline-column",
55
- style: { flex: band.units },
112
+ style: { gridColumn: `span ${band.units}` },
56
113
  children: /* @__PURE__ */ jsx(Text, { size: "xs", weight: "medium", children: band.label })
57
114
  },
58
115
  index
@@ -64,15 +121,15 @@ const RangeTimeline = React.forwardRef(
64
121
  "div",
65
122
  {
66
123
  className: "ui-range-timeline-column",
67
- style: { flex: column.units },
124
+ style: { gridColumn: `span ${column.units}` },
68
125
  children: /* @__PURE__ */ jsx(Text, { size: "xs", children: column.label })
69
126
  },
70
127
  index
71
128
  )) })
72
129
  ] }),
73
- /* @__PURE__ */ jsxs("div", { className: "ui-range-timeline-body", children: [
74
- (bordered || columns.some((column) => column.muted)) && // Decorative: the same flex/units layout as the header columns, laid once behind every
75
- // row so each vertical rule runs the full body height exactly under its header column.
130
+ /* @__PURE__ */ jsxs("div", { className: "ui-range-timeline-body", role: nested ? "list" : void 0, children: [
131
+ (bordered || columns.some((column) => column.muted)) && // Decorative: the same unit tracks as the header columns, laid once behind every row so
132
+ // each vertical rule runs the full body height exactly under its header column.
76
133
  /* @__PURE__ */ jsxs("div", { className: "ui-range-timeline-grid", "aria-hidden": "true", children: [
77
134
  /* @__PURE__ */ jsx("div", {}),
78
135
  /* @__PURE__ */ jsx("div", { className: "ui-range-timeline-columns", children: columns.map((column, index) => /* @__PURE__ */ jsx(
@@ -80,108 +137,153 @@ const RangeTimeline = React.forwardRef(
80
137
  {
81
138
  className: "ui-range-timeline-grid-column",
82
139
  "data-muted": column.muted ? "true" : void 0,
83
- style: { flex: column.units }
140
+ style: { gridColumn: `span ${column.units}` }
84
141
  },
85
142
  index
86
143
  )) })
87
144
  ] }),
88
- rows.map((row) => {
145
+ visibleRows.map((row, index) => {
146
+ const idBase = `${reactId}-row-${rows.indexOf(row)}`;
89
147
  const delta = preview?.id === row.id ? preview.delta : 0;
90
148
  const start = Math.min(row.end, row.start + (preview?.edge === "start" ? delta : 0));
91
149
  const end = Math.max(row.start, row.end + (preview?.edge === "end" ? delta : 0));
92
150
  const left = Math.max(0, start), right = Math.min(units, end + 1);
93
- return /* @__PURE__ */ jsxs("div", { className: "ui-range-timeline-row", children: [
94
- /* @__PURE__ */ jsx("div", { className: "ui-range-timeline-label", children: row.label }),
95
- /* @__PURE__ */ jsxs("div", { className: "ui-range-timeline-track", children: [
96
- today != null && today >= 0 && today < units && /* @__PURE__ */ jsx(
97
- "span",
98
- {
99
- "aria-hidden": "true",
100
- className: "ui-range-timeline-today",
101
- style: { insetInlineStart: `${(today + 0.5) / units * 100}%` }
102
- }
103
- ),
104
- right <= left && (end < 0 ? /* @__PURE__ */ jsxs("span", { className: "ui-range-timeline-outside", "data-direction": "before", children: [
105
- /* @__PURE__ */ jsx(
106
- ChevronLeft,
107
- {
108
- "aria-hidden": "true",
109
- className: "ui-range-timeline-outside-icon"
110
- }
111
- ),
112
- /* @__PURE__ */ jsx(Text, { size: "xs", tone: "muted", children: t("rangeTimeline.outsideBefore") })
113
- ] }) : /* @__PURE__ */ jsxs("span", { className: "ui-range-timeline-outside", "data-direction": "after", children: [
114
- /* @__PURE__ */ jsx(Text, { size: "xs", tone: "muted", children: t("rangeTimeline.outsideAfter") }),
115
- /* @__PURE__ */ jsx(
116
- ChevronRight,
151
+ return /* @__PURE__ */ jsxs(
152
+ "div",
153
+ {
154
+ className: "ui-range-timeline-row",
155
+ ...nested && {
156
+ role: "listitem",
157
+ "aria-level": depthOf(row) + 1,
158
+ "aria-setsize": siblingsOf(index).size,
159
+ "aria-posinset": siblingsOf(index).position
160
+ },
161
+ children: [
162
+ nested ? /* @__PURE__ */ jsxs(
163
+ "div",
117
164
  {
118
- "aria-hidden": "true",
119
- className: "ui-range-timeline-outside-icon"
165
+ className: "ui-range-timeline-label",
166
+ "data-nested": "true",
167
+ style: { "--range-timeline-depth": depthOf(row) },
168
+ children: [
169
+ parents.includes(row.id) ? /* @__PURE__ */ jsxs(
170
+ Button,
171
+ {
172
+ variant: "ghost",
173
+ size: "icon-xs",
174
+ className: "ui-range-timeline-disclosure",
175
+ "aria-expanded": isExpanded(row.id),
176
+ "aria-labelledby": `${idBase}-toggle ${idBase}-label`,
177
+ onClick: () => toggleRow(row.id),
178
+ children: [
179
+ /* @__PURE__ */ jsx("span", { id: `${idBase}-toggle`, hidden: true, children: t("rangeTimeline.childRows") }),
180
+ isExpanded(row.id) ? /* @__PURE__ */ jsx(ChevronDown, { "aria-hidden": "true" }) : /* @__PURE__ */ jsx(
181
+ ChevronRight,
182
+ {
183
+ "aria-hidden": "true",
184
+ className: "ui-range-timeline-disclosure-collapsed"
185
+ }
186
+ )
187
+ ]
188
+ }
189
+ ) : /* @__PURE__ */ jsx("span", { "aria-hidden": "true", className: "ui-range-timeline-disclosure-spacer" }),
190
+ /* @__PURE__ */ jsx("div", { id: `${idBase}-label`, className: "ui-range-timeline-label-content", children: row.label })
191
+ ]
120
192
  }
121
- )
122
- ] })),
123
- right > left && /* @__PURE__ */ jsx(
124
- "div",
125
- {
126
- className: "ui-range-timeline-bar",
127
- style: {
128
- insetInlineStart: `${left / units * 100}%`,
129
- inlineSize: `${(right - left) / units * 100}%`
130
- },
131
- children: onRangeChange && (right - left) * Math.max(1, columns.length) >= units && ["start", "end"].filter(
132
- (edge) => edge === "start" ? start >= 0 && start < units : end >= 0 && end < units
133
- ).map((edge) => /* @__PURE__ */ jsx(
134
- Button,
193
+ ) : /* @__PURE__ */ jsx("div", { className: "ui-range-timeline-label", children: row.label }),
194
+ /* @__PURE__ */ jsxs("div", { className: "ui-range-timeline-track", children: [
195
+ today != null && today >= 0 && today < units && /* @__PURE__ */ jsx(
196
+ "span",
135
197
  {
136
- variant: "ghost",
137
- size: "icon-xs",
138
- className: "ui-range-timeline-handle",
139
- "data-edge": edge,
140
- "aria-label": edge === "start" ? row.startLabel : row.endLabel,
141
- onPointerDown: (event) => {
142
- const width = track.current?.getBoundingClientRect().width ?? 0;
143
- if (!width || event.button !== 0) return;
144
- event.currentTarget.setPointerCapture(event.pointerId);
145
- drag.current = {
146
- id: row.id,
147
- edge,
148
- x: event.clientX,
149
- width,
150
- direction: getComputedStyle(event.currentTarget).direction === "rtl" ? -1 : 1
151
- };
152
- },
153
- onPointerMove: (event) => {
154
- if (drag.current)
155
- setPreview({
156
- id: drag.current.id,
157
- edge: drag.current.edge,
158
- delta: deltaAt(event.clientX)
159
- });
160
- },
161
- onPointerUp: complete,
162
- onPointerCancel: cancel,
163
- onLostPointerCapture: cancel,
164
- onKeyDown: (event) => {
165
- if (event.key === "Escape") {
166
- cancel();
167
- return;
168
- }
169
- const step = event.key === "ArrowRight" ? 1 : event.key === "ArrowLeft" ? -1 : 0;
170
- if (!step) return;
171
- event.preventDefault();
172
- const direction = getComputedStyle(event.currentTarget).direction === "rtl" ? -1 : 1;
173
- const delta2 = step * direction;
174
- if (edge === "start" ? row.start + delta2 <= row.end : row.end + delta2 >= row.start)
175
- onRangeChange(row.id, edge, delta2);
198
+ "aria-hidden": "true",
199
+ className: "ui-range-timeline-today",
200
+ style: { insetInlineStart: `${(today + 0.5) / units * 100}%` }
201
+ }
202
+ ),
203
+ right <= left && (end < 0 ? /* @__PURE__ */ jsxs("span", { className: "ui-range-timeline-outside", "data-direction": "before", children: [
204
+ /* @__PURE__ */ jsx(
205
+ ChevronLeft,
206
+ {
207
+ "aria-hidden": "true",
208
+ className: "ui-range-timeline-outside-icon"
209
+ }
210
+ ),
211
+ /* @__PURE__ */ jsx(Text, { size: "xs", tone: "muted", children: t("rangeTimeline.outsideBefore") })
212
+ ] }) : /* @__PURE__ */ jsxs("span", { className: "ui-range-timeline-outside", "data-direction": "after", children: [
213
+ /* @__PURE__ */ jsx(Text, { size: "xs", tone: "muted", children: t("rangeTimeline.outsideAfter") }),
214
+ /* @__PURE__ */ jsx(
215
+ ChevronRight,
216
+ {
217
+ "aria-hidden": "true",
218
+ className: "ui-range-timeline-outside-icon"
219
+ }
220
+ )
221
+ ] })),
222
+ right > left && /* @__PURE__ */ jsx(
223
+ "div",
224
+ {
225
+ className: "ui-range-timeline-bar",
226
+ style: {
227
+ insetInlineStart: `${left / units * 100}%`,
228
+ inlineSize: `${(right - left) / units * 100}%`
176
229
  },
177
- children: /* @__PURE__ */ jsx(GripVertical, { "aria-hidden": "true" })
178
- },
179
- edge
180
- ))
181
- }
182
- )
183
- ] })
184
- ] }, row.id);
230
+ children: onRangeChange && (right - left) * Math.max(1, columns.length) >= units && ["start", "end"].filter(
231
+ (edge) => edge === "start" ? start >= 0 && start < units : end >= 0 && end < units
232
+ ).map((edge) => /* @__PURE__ */ jsx(
233
+ Button,
234
+ {
235
+ variant: "ghost",
236
+ size: "icon-xs",
237
+ className: "ui-range-timeline-handle",
238
+ "data-edge": edge,
239
+ "aria-label": edge === "start" ? row.startLabel : row.endLabel,
240
+ onPointerDown: (event) => {
241
+ const width = track.current?.getBoundingClientRect().width ?? 0;
242
+ if (!width || event.button !== 0) return;
243
+ event.currentTarget.setPointerCapture(event.pointerId);
244
+ drag.current = {
245
+ id: row.id,
246
+ edge,
247
+ x: event.clientX,
248
+ width,
249
+ direction: getComputedStyle(event.currentTarget).direction === "rtl" ? -1 : 1
250
+ };
251
+ },
252
+ onPointerMove: (event) => {
253
+ if (drag.current)
254
+ setPreview({
255
+ id: drag.current.id,
256
+ edge: drag.current.edge,
257
+ delta: deltaAt(event.clientX)
258
+ });
259
+ },
260
+ onPointerUp: complete,
261
+ onPointerCancel: cancel,
262
+ onLostPointerCapture: cancel,
263
+ onKeyDown: (event) => {
264
+ if (event.key === "Escape") {
265
+ cancel();
266
+ return;
267
+ }
268
+ const step = event.key === "ArrowRight" ? 1 : event.key === "ArrowLeft" ? -1 : 0;
269
+ if (!step) return;
270
+ event.preventDefault();
271
+ const direction = getComputedStyle(event.currentTarget).direction === "rtl" ? -1 : 1;
272
+ const delta2 = step * direction;
273
+ if (edge === "start" ? row.start + delta2 <= row.end : row.end + delta2 >= row.start)
274
+ onRangeChange(row.id, edge, delta2);
275
+ },
276
+ children: /* @__PURE__ */ jsx(GripVertical, { "aria-hidden": "true" })
277
+ },
278
+ edge
279
+ ))
280
+ }
281
+ )
282
+ ] })
283
+ ]
284
+ },
285
+ row.id
286
+ );
185
287
  })
186
288
  ] })
187
289
  ]
@@ -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": "27.5.0",
3
+ "version": "27.7.0",
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,
@@ -607,6 +607,7 @@
607
607
  "trendCurrent": "current"
608
608
  },
609
609
  "rangeTimeline": {
610
+ "childRows": "Child rows",
610
611
  "outsideAfter": "After the visible range",
611
612
  "outsideBefore": "Before the visible range",
612
613
  "schedule": "Schedule"
@@ -594,6 +594,7 @@
594
594
  "trendCurrent": "現在"
595
595
  },
596
596
  "rangeTimeline": {
597
+ "childRows": "子の行",
597
598
  "outsideAfter": "表示範囲より後",
598
599
  "outsideBefore": "表示範囲より前",
599
600
  "schedule": "スケジュール"
@@ -595,6 +595,7 @@
595
595
  "trendCurrent": "hiện tại"
596
596
  },
597
597
  "rangeTimeline": {
598
+ "childRows": "Hàng con",
598
599
  "outsideAfter": "Sau khoảng đang xem",
599
600
  "outsideBefore": "Trước khoảng đang xem",
600
601
  "schedule": "Lịch công việc"
@@ -745,8 +745,9 @@ export type CalendarProp = DayPickerProps & CalendarFooterProp & {
745
745
  *
746
746
  * Default `true` — the owner's call, from a real screen: with no ruling the month read as a
747
747
  * cloud of numbers and was "very hard to read". Pass `bordered={false}` for the floating-button
748
- * look. The line colour is `--calendar-grid-border-color` (default `--input`, the ≥3:1 tier;
749
- * the decorative `--border` measured 1.15:1 and was invisible as a 1px ruling).
748
+ * look. The line colour is `--calendar-grid-border-color` (default `--input` at 0.5 alpha,
749
+ * 1.74:1 light / 1.95:1 dark on the popover; the full `--input` read too dark, and the
750
+ * decorative `--border` measured 1.15:1 and was invisible as a 1px ruling).
750
751
  */
751
752
  bordered?: boolean;
752
753
  /** Decorate a day cell — 祝日, a booked day, a deadline. @see CalendarCellRenderProp */
@@ -2950,7 +2950,7 @@ export declare const COMPONENT_PROP_REGISTRY: {
2950
2950
  }, {
2951
2951
  readonly field: "rows";
2952
2952
  readonly local: true;
2953
- readonly reason: "Consumer-supplied interval records preserve true inclusive endpoints independently of clipping.";
2953
+ readonly reason: "Consumer-supplied interval records preserve true inclusive endpoints independently of clipping; optional `depth` nests them (flat, depth-first; a row is a parent when the next row is deeper).";
2954
2954
  }, {
2955
2955
  readonly field: "today";
2956
2956
  readonly local: true;
@@ -2959,6 +2959,18 @@ export declare const COMPONENT_PROP_REGISTRY: {
2959
2959
  readonly field: "onRangeChange";
2960
2960
  readonly local: true;
2961
2961
  readonly reason: "An endpoint movement command, not an internally owned value; the consumer commits its row data.";
2962
+ }, {
2963
+ readonly field: "expandedValues";
2964
+ readonly local: true;
2965
+ readonly reason: "Controlled expanded parent row ids (gh#724) — the same spelling as `Tree expandedValues` (antd `expandedKeys` / Table `expandedRowKeys`), per docs/DESIGN-AUTHORITY.md.";
2966
+ }, {
2967
+ readonly field: "defaultExpandedValues";
2968
+ readonly local: true;
2969
+ readonly reason: "Uncontrolled initial expanded parents, same spelling as `Tree`; omitted, every parent starts expanded so adding `depth` never hides a row.";
2970
+ }, {
2971
+ readonly field: "onExpandedValuesChange";
2972
+ readonly local: true;
2973
+ readonly reason: "Next expanded parent ids, fired for controlled and uncontrolled timelines alike — the `Tree onExpandedValuesChange` callback.";
2962
2974
  }];
2963
2975
  };
2964
2976
  readonly ResponsiveGridProp: {
@@ -3460,7 +3460,7 @@ const COMPONENT_PROP_REGISTRY = {
3460
3460
  {
3461
3461
  field: "rows",
3462
3462
  local: true,
3463
- reason: "Consumer-supplied interval records preserve true inclusive endpoints independently of clipping."
3463
+ reason: "Consumer-supplied interval records preserve true inclusive endpoints independently of clipping; optional `depth` nests them (flat, depth-first; a row is a parent when the next row is deeper)."
3464
3464
  },
3465
3465
  {
3466
3466
  field: "today",
@@ -3471,6 +3471,21 @@ const COMPONENT_PROP_REGISTRY = {
3471
3471
  field: "onRangeChange",
3472
3472
  local: true,
3473
3473
  reason: "An endpoint movement command, not an internally owned value; the consumer commits its row data."
3474
+ },
3475
+ {
3476
+ field: "expandedValues",
3477
+ local: true,
3478
+ reason: "Controlled expanded parent row ids (gh#724) \u2014 the same spelling as `Tree expandedValues` (antd `expandedKeys` / Table `expandedRowKeys`), per docs/DESIGN-AUTHORITY.md."
3479
+ },
3480
+ {
3481
+ field: "defaultExpandedValues",
3482
+ local: true,
3483
+ reason: "Uncontrolled initial expanded parents, same spelling as `Tree`; omitted, every parent starts expanded so adding `depth` never hides a row."
3484
+ },
3485
+ {
3486
+ field: "onExpandedValuesChange",
3487
+ local: true,
3488
+ reason: "Next expanded parent ids, fired for controlled and uncontrolled timelines alike \u2014 the `Tree onExpandedValuesChange` callback."
3474
3489
  }
3475
3490
  ]
3476
3491
  },
@@ -2229,13 +2229,13 @@
2229
2229
 
2230
2230
  .ui-calendar[data-bordered="true"] .ui-calendar-day,
2231
2231
  .ui-calendar[data-bordered="true"] .ui-calendar-weekday {
2232
- border-inline-start: 1px solid var(--calendar-grid-border-color, hsl(var(--input)));
2233
- border-block-start: 1px solid var(--calendar-grid-border-color, hsl(var(--input)));
2232
+ border-inline-start: 1px solid var(--calendar-grid-border-color, hsl(var(--input) / 0.5));
2233
+ border-block-start: 1px solid var(--calendar-grid-border-color, hsl(var(--input) / 0.5));
2234
2234
  }
2235
2235
 
2236
2236
  .ui-calendar[data-bordered="true"] .ui-calendar-week,
2237
2237
  .ui-calendar[data-bordered="true"] .ui-calendar-weekdays {
2238
- border-inline-end: 1px solid var(--calendar-grid-border-color, hsl(var(--input)));
2238
+ border-inline-end: 1px solid var(--calendar-grid-border-color, hsl(var(--input) / 0.5));
2239
2239
  }
2240
2240
 
2241
2241
  .ui-calendar[data-bordered="true"] .ui-calendar-weekday {
@@ -2247,7 +2247,7 @@
2247
2247
  }
2248
2248
 
2249
2249
  .ui-calendar[data-bordered="true"] .ui-calendar-grid {
2250
- border-block-end: 1px solid var(--calendar-grid-border-color, hsl(var(--input)));
2250
+ border-block-end: 1px solid var(--calendar-grid-border-color, hsl(var(--input) / 0.5));
2251
2251
  }
2252
2252
 
2253
2253
  .ui-calendar[data-bordered="true"] .ui-calendar-day.invisible {
@@ -2268,8 +2268,8 @@
2268
2268
 
2269
2269
  .ui-calendar[data-bordered="true"] .ui-calendar-week-number,
2270
2270
  .ui-calendar[data-bordered="true"] .ui-calendar-week-number-header {
2271
- border-inline-start: 1px solid var(--calendar-grid-border-color, hsl(var(--input)));
2272
- border-block-start: 1px solid var(--calendar-grid-border-color, hsl(var(--input)));
2271
+ border-inline-start: 1px solid var(--calendar-grid-border-color, hsl(var(--input) / 0.5));
2272
+ border-block-start: 1px solid var(--calendar-grid-border-color, hsl(var(--input) / 0.5));
2273
2273
  }
2274
2274
 
2275
2275
  .ui-calendar[data-bordered="true"] .ui-calendar-day {
@@ -1351,8 +1351,10 @@
1351
1351
  padding: var(--space-2);
1352
1352
  border-inline-end: 1px solid hsl(var(--range-timeline-border-color));
1353
1353
  }
1354
+
1354
1355
  .ui-range-timeline-columns {
1355
- display: flex;
1356
+ display: grid;
1357
+ grid-template-columns: repeat(var(--range-timeline-units, 1), minmax(0, 1fr));
1356
1358
  }
1357
1359
  .ui-range-timeline-column {
1358
1360
  min-inline-size: 0;
@@ -1390,12 +1392,12 @@
1390
1392
  .ui-range-timeline[data-bordered="true"] .ui-range-timeline-column,
1391
1393
  .ui-range-timeline[data-bordered="true"] .ui-range-timeline-label,
1392
1394
  .ui-range-timeline[data-bordered="true"] .ui-range-timeline-grid-column {
1393
- border-inline-end-color: var(--range-timeline-grid-color, hsl(var(--input)));
1395
+ border-inline-end-color: var(--range-timeline-grid-color, hsl(var(--input) / 0.5));
1394
1396
  }
1395
1397
  .ui-range-timeline[data-bordered="true"] .ui-range-timeline-header + .ui-range-timeline-header,
1396
1398
  .ui-range-timeline[data-bordered="true"] .ui-range-timeline-row {
1397
1399
  border-block-start: var(--range-timeline-grid-width) solid
1398
- var(--range-timeline-grid-color, hsl(var(--input)));
1400
+ var(--range-timeline-grid-color, hsl(var(--input) / 0.5));
1399
1401
  }
1400
1402
  .ui-range-timeline-outside {
1401
1403
  position: absolute;
@@ -1452,6 +1454,36 @@
1452
1454
  inset-block: 0;
1453
1455
  border-inline-start: 1px solid hsl(var(--primary));
1454
1456
  }
1457
+
1458
+ .ui-range-timeline-label[data-nested="true"] {
1459
+ display: flex;
1460
+ align-items: center;
1461
+ gap: var(--space-1);
1462
+ padding-inline-start: calc(
1463
+ var(--space-2) + var(--range-timeline-depth, 0) * var(--range-timeline-indent-width)
1464
+ );
1465
+ }
1466
+ .ui-range-timeline-label-content {
1467
+ flex: 1;
1468
+ min-inline-size: 0;
1469
+ }
1470
+ .ui-range-timeline-disclosure,
1471
+ .ui-range-timeline-disclosure-spacer {
1472
+ flex-shrink: 0;
1473
+ }
1474
+ .ui-range-timeline-disclosure-spacer {
1475
+ inline-size: var(--control-height-xs);
1476
+ }
1477
+
1478
+ @media (pointer: coarse) {
1479
+ .ui-range-timeline-disclosure-spacer {
1480
+ inline-size: var(--control-height-default);
1481
+ }
1482
+ }
1483
+
1484
+ [dir="rtl"] .ui-range-timeline-disclosure-collapsed {
1485
+ transform: scaleX(-1);
1486
+ }
1455
1487
  }
1456
1488
 
1457
1489
  @layer components {
@@ -183,4 +183,6 @@
183
183
  --range-timeline-grid-width: var(--stroke-hairline);
184
184
 
185
185
  --range-timeline-muted-column-background: initial;
186
+
187
+ --range-timeline-indent-width: var(--space-4);
186
188
  }
@@ -298,6 +298,23 @@ bottomEnd | none` (`TablePaginationPositionProp`, default `['bottomEnd']`), the
298
298
  purpose: omitted, `flow="columns"` keeps `start` and `flow="rows"` keeps the grid's stretch, so
299
299
  no existing grid moves; antd's `Row` default is `top` in every case.
300
300
 
301
+ - **`RangeTimeline` nested rows take `Tree`'s spelling, not antd's (gh#724).** antd has no Gantt;
302
+ the nearest names are `Tree expandedKeys` / `defaultExpandedKeys` / `onExpand` and `Table
303
+ expandable.expandedRowKeys` / `onExpandedRowsChange`. This library already mapped the Tree ones
304
+ onto its controlled vocabulary as `expandedValues` / `defaultExpandedValues` /
305
+ `onExpandedValuesChange`, and a second spelling of the same axis is what
306
+ `check:prop-vocabulary` exists to prevent — so `RangeTimeline` reuses those three names exactly
307
+ (the values are row `id`s). The shape is also `Tree`'s: flat rows plus `depth`, the component
308
+ owns the indent (`--range-timeline-indent-width`) and the disclosure. Two deliberate differences
309
+ from `Tree`: (1) a row is a parent when the NEXT row is deeper — derived from the depth-first list,
310
+ never from a `hasChildren` flag, so a disclosure can never promise children that are not there;
311
+ (2) omitted `defaultExpandedValues` starts every parent EXPANDED (a `Tree` starts closed), so
312
+ adding `depth` to an existing schedule never hides a row. The disclosure is a real `Button`
313
+ (the row is not a focusable tree item, so APG's one-tab-stop rule does not apply), and the rows
314
+ are a `list` of `listitem`s with `aria-level` / `aria-setsize` / `aria-posinset` — the lightest
315
+ role that carries a level without promising the `tree` / `treegrid` arrow-key model. A timeline
316
+ with no `depth > 0` keeps its pre-#724 markup byte for byte.
317
+
301
318
  **A knob that only a fork could reach is not parity either.** antd's `components`,
302
319
  `filterDropdown`, `classNames`/`styles` semantic maps and `prefixCls` all exist to let a consumer
303
320
  replace the rendered markup. This library answers that layer with tokens (cardinal rule #45), so
@@ -200,6 +200,42 @@ export default function Demo() {
200
200
  </CardContent>
201
201
  </Card>
202
202
 
203
+ <Card>
204
+ <CardHeader>
205
+ <CardTitle level={2}>月をまたぐ日単位の軸(bands)</CardTitle>
206
+ <CardDescription>
207
+ 9月25日から10月7日まで。bands
208
+ の月の境界は、その月の1日の列の境界にぴったり重なります。
209
+ 始まりが月の途中なので、最初の月の帯は6日分だけの幅です。
210
+ </CardDescription>
211
+ </CardHeader>
212
+ <CardContent>
213
+ <RangeTimeline
214
+ label={t("rangeTimeline.schedule")}
215
+ bands={[
216
+ { label: monthLabel(8), units: 6 },
217
+ { label: monthLabel(9), units: 7 },
218
+ ]}
219
+ columns={Array.from({ length: 13 }, (_, index) => {
220
+ const date = new Date(Date.UTC(2026, 8, 25 + index));
221
+ return {
222
+ label: new Intl.NumberFormat(locale).format(date.getUTCDate()),
223
+ units: 1,
224
+ muted: date.getUTCDay() === 0 || date.getUTCDay() === 6,
225
+ };
226
+ })}
227
+ rows={[
228
+ { id: "migration", label: "移行", start: 3, end: 8 },
229
+ { id: "october", label: "10月の作業", start: 6, end: 12 },
230
+ ].map((row) => ({
231
+ ...row,
232
+ startLabel: dayLabel(25 + row.start),
233
+ endLabel: dayLabel(25 + row.end),
234
+ }))}
235
+ />
236
+ </CardContent>
237
+ </Card>
238
+
203
239
  <Card>
204
240
  <CardHeader>
205
241
  <CardTitle level={2}>月単位の軸(列ごとに units が異なる)</CardTitle>
@@ -257,6 +293,47 @@ export default function Demo() {
257
293
  />
258
294
  </CardContent>
259
295
  </Card>
296
+
297
+ <Card>
298
+ <CardHeader>
299
+ <CardTitle level={2}>親子のある作業(depth と折りたたみ)</CardTitle>
300
+ <CardDescription>
301
+ 行を深さ優先の順に並べ、depth
302
+ を渡すと、ラベルの列の中で段ごとに字下げされます。次の行が
303
+ より深い行は親になり、開閉ボタンが付きます。「実装」は defaultExpandedValues
304
+ に含まれていないため折りたたまれており、子の行もそのバーも表示されません。
305
+ </CardDescription>
306
+ </CardHeader>
307
+ <CardContent>
308
+ <RangeTimeline
309
+ label={t("rangeTimeline.schedule")}
310
+ today={4}
311
+ columns={Array.from({ length: 14 }, (_, index) => {
312
+ const weekday = new Date(Date.UTC(2026, 8, index + 1)).getUTCDay();
313
+ return {
314
+ label: new Intl.NumberFormat(locale).format(index + 1),
315
+ units: 1,
316
+ muted: weekday === 0 || weekday === 6,
317
+ };
318
+ })}
319
+ defaultExpandedValues={["design", "api"]}
320
+ rows={[
321
+ { id: "design", label: "設計", depth: 0, start: 0, end: 5 },
322
+ { id: "screens", label: "画面設計", depth: 1, start: 0, end: 2 },
323
+ { id: "api", label: "API設計", depth: 1, start: 2, end: 5 },
324
+ { id: "endpoints", label: "エンドポイント定義", depth: 2, start: 2, end: 3 },
325
+ { id: "build", label: "実装", depth: 0, start: 6, end: 12 },
326
+ { id: "frontend", label: "フロントエンド", depth: 1, start: 6, end: 10 },
327
+ { id: "backend", label: "バックエンド", depth: 1, start: 6, end: 12 },
328
+ { id: "release", label: "リリース", depth: 0, start: 13, end: 13 },
329
+ ].map((row) => ({
330
+ ...row,
331
+ startLabel: dayLabel(row.start + 1),
332
+ endLabel: dayLabel(row.end + 1),
333
+ }))}
334
+ />
335
+ </CardContent>
336
+ </Card>
260
337
  </Flex>
261
338
  </PageContainer>
262
339
  );
@@ -77,9 +77,8 @@ export default function Demo() {
77
77
  罫線 (bordered) · 既定で表示 / bordered={"{false}"} で解除
78
78
  </CardTitle>
79
79
  <CardDescription>
80
- 日と日の間に罫線を引くのが既定。線色は --calendar-grid-border-color (既定
81
- --input、背景に対し 3:1
82
- 以上)。選択日・今日・範囲の始点/中間/終点・無効日・前後月の日が
80
+ 日と日の間に罫線を引くのが既定。線色は --calendar-grid-border-color (既定 --input の
81
+ 50%、背景に対し約 1.7:1)。選択日・今日・範囲の始点/中間/終点・無効日・前後月の日が
83
82
  罫線の中でも判別できる。罫線なしの浮いたボタン表示にしたい場合のみ bordered=
84
83
  {"{false}"} を渡す。
85
84
  </CardDescription>
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@godxjp/ui",
3
- "version": "27.5.0",
4
- "godxUiMcp": "27.5.0",
3
+ "version": "27.7.0",
4
+ "godxUiMcp": "27.7.0",
5
5
  "license": "Apache-2.0",
6
6
  "repository": {
7
7
  "type": "git",
@@ -16,12 +16,14 @@ import {
16
16
  existsSync,
17
17
  mkdirSync,
18
18
  readFileSync,
19
+ realpathSync,
19
20
  renameSync,
20
21
  statSync,
21
22
  unlinkSync,
22
23
  writeFileSync,
23
24
  } from "node:fs";
24
- import { basename, dirname, join } from "node:path";
25
+ import { homedir } from "node:os";
26
+ import { basename, dirname, join, resolve } from "node:path";
25
27
  import { fileURLToPath } from "node:url";
26
28
 
27
29
  /** This package's own root — the source of the version we stamp with. */
@@ -425,6 +427,83 @@ export function ensureMcpJson(root) {
425
427
  return created ? "created" : "added";
426
428
  }
427
429
 
430
+ /**
431
+ * Registrations of the catalog server OUTSIDE the project — READ-ONLY (gh#722).
432
+ *
433
+ * `.mcp.json` is not the only place an agent's MCP servers come from. Claude Code keeps
434
+ * user-scoped servers in the top-level `mcpServers` of `~/.claude.json` (under
435
+ * `$CLAUDE_CONFIG_DIR` when that is set) and local-scoped ones — private to one checkout — under
436
+ * `projects[<absolute project path>].mcpServers` of the same file. Both are live beside the
437
+ * project's entry. Measured: a user-scoped `godxjp-ui` serving a ≤ 21.0.0 catalog answered next
438
+ * to the project's pinned `godx-ui`, and an agent took a removed component's page from it.
439
+ *
440
+ * That file belongs to the user, spans every project, and is not ours to edit: this only READS
441
+ * it. A missing, unreadable or malformed file yields nothing, silently. Returns
442
+ * `{ key, scope, pin }` per entry — never `env`, which is where secrets live.
443
+ */
444
+ export function findOutOfProjectUiMcp(
445
+ root,
446
+ configDir = process.env.CLAUDE_CONFIG_DIR || homedir(),
447
+ ) {
448
+ try {
449
+ const read = readJsonFile(join(configDir, ".claude.json"));
450
+ if (read.state !== "ok") return [];
451
+ const found = [];
452
+ const collect = (servers, scope) => {
453
+ if (!servers || typeof servers !== "object" || Array.isArray(servers)) return;
454
+ for (const [key, entry] of Object.entries(servers)) {
455
+ if (!runsUiMcp(entry)) continue;
456
+ const parts = [entry.command, ...(Array.isArray(entry.args) ? entry.args : [])];
457
+ const pin = parts.find(
458
+ (part) => typeof part === "string" && /(^|\/)@godxjp\/ui-mcp(@|$)/.test(part),
459
+ );
460
+ found.push({ key, scope, pin });
461
+ }
462
+ };
463
+ collect(read.json.mcpServers, "user");
464
+ const projects = read.json.projects;
465
+ if (projects && typeof projects === "object" && !Array.isArray(projects)) {
466
+ const paths = new Set([resolve(root)]);
467
+ try {
468
+ paths.add(realpathSync(root));
469
+ } catch {
470
+ // A root that cannot be resolved is looked up by its given path only.
471
+ }
472
+ for (const path of paths) collect(projects[path]?.mcpServers, "local");
473
+ }
474
+ return found;
475
+ } catch {
476
+ return [];
477
+ }
478
+ }
479
+
480
+ const shellWord = (word) => (/^[\w.@/-]+$/.test(word) ? word : `'${word.replace(/'/g, "'\\''")}'`);
481
+
482
+ /**
483
+ * What `sync-rules` and `init-agent` print about out-of-project registrations: key, pin, that it
484
+ * was left untouched, and the command that updates it. Empty string when there are none.
485
+ */
486
+ export function outOfProjectUiMcpReport(root, configDir) {
487
+ const found = findOutOfProjectUiMcp(root, configDir);
488
+ if (found.length === 0) return "";
489
+ const expected = mcpServerFor(root).args[0];
490
+ let out =
491
+ `\n @godxjp/ui → @godxjp/ui-mcp is ALSO registered outside this project, in Claude Code's ` +
492
+ `.claude.json.\n It answers beside the project's .mcp.json entry (${expected}) and an agent ` +
493
+ `can call either — the tool names differ only by the key.\n`;
494
+ for (const { key, scope, pin } of found) {
495
+ const k = shellWord(key);
496
+ const runs = /@godxjp\/ui-mcp@\S+$/.test(pin ?? "")
497
+ ? pin
498
+ : `${pin ?? "@godxjp/ui-mcp"} (unpinned: resolves the latest release)`;
499
+ out +=
500
+ ` • "${key}" (${scope} scope) runs ${runs} — outside the project, left untouched.\n` +
501
+ ` remove it: claude mcp remove ${k} -s ${scope}\n` +
502
+ ` or re-pin: claude mcp remove ${k} -s ${scope} && claude mcp add ${k} -s ${scope} -- npx ${expected}\n`;
503
+ }
504
+ return out;
505
+ }
506
+
428
507
  /** Ensure `.claude/settings.json` has the audit PostToolUse + workflow SessionStart hooks. */
429
508
  export function ensureClaudeHooks(root) {
430
509
  const path = join(root, ".claude", "settings.json");
package/scripts/cli.mjs CHANGED
@@ -34,7 +34,11 @@ const HELP = {
34
34
  Refresh the package-owned agent files for the installed @godxjp/ui version: the godx-ui entry in
35
35
  .mcp.json, the managed CLAUDE.md block, .claude/godxjp-ui-workflow.md and .ai/rules/godxjp-ui.md.
36
36
  It is the postinstall step, run by hand — use it when the app installs with ignore-scripts=true,
37
- where postinstall never runs and the rules go stale. Silent no-op in CI and when opted out.`,
37
+ where postinstall never runs and the rules go stale. Silent no-op in CI and when opted out.
38
+
39
+ It also READS Claude Code's ~/.claude.json and reports any @godxjp/ui-mcp registered there (user
40
+ scope, or local scope for this project): key, pin, and the claude mcp command that updates it.
41
+ That file is outside the project and is never written; env values are never printed.`,
38
42
  audit: `godxjp-ui audit [dir …] [--changed] [--format json] [--quiet] [--rules]
39
43
 
40
44
  Static UI-standardization audit over source (regex, no browser, fast). Exits non-zero on errors.
@@ -4,6 +4,7 @@ import {
4
4
  ensureClaudeHooks,
5
5
  ensureClaudeMd,
6
6
  ensureMcpJson,
7
+ outOfProjectUiMcpReport,
7
8
  shouldSkip,
8
9
  writeWorkflowMd,
9
10
  } from "./_agent-setup.mjs";
@@ -28,6 +29,8 @@ console.log(
28
29
  );
29
30
  console.log(` • .claude/godxjp-ui-workflow.md — ${md ? "created" : "already present"}`);
30
31
  console.log(` • CLAUDE.md — godxjp-ui mandate (${claudeMd})`);
32
+ const outside = outOfProjectUiMcpReport(root);
33
+ if (outside) console.log(outside);
31
34
  console.log(`
32
35
  The auto-audit hook now runs on every Write/Edit of a .tsx file and feeds findings
33
36
  back to the agent — it cannot skip the audit. Restart your agent to load the MCP + hooks.
@@ -8,6 +8,7 @@ import {
8
8
  ensureClaudeMd,
9
9
  ensureMcpJson,
10
10
  ensureConsumerRules,
11
+ outOfProjectUiMcpReport,
11
12
  refreshGuineaPigSkill,
12
13
  shouldSkip,
13
14
  writeWorkflowMd,
@@ -25,6 +26,10 @@ try {
25
26
  if (r.startsWith("left untouched") || r.startsWith("present (custom godx-ui")) {
26
27
  console.log(`\n @godxjp/ui → .mcp.json ${r}\n`);
27
28
  }
29
+ // A registration outside the project is reported whatever the project file says, before any of
30
+ // the quiet exits below: it is live beside ours and nothing here will ever change it (gh#722).
31
+ const outside = outOfProjectUiMcpReport(root);
32
+ if (outside) console.log(outside);
28
33
  // The mandate is plain text the agent reads every turn (CLAUDE.md block + workflow file). It
29
34
  // changes nothing in the dev loop, so it is installed by default: an agent that never saw the
30
35
  // but no mandate). Only the hooks — which DO change the loop — stay behind `init-agent`.