@godxjp/ui 27.6.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)
@@ -73,7 +127,7 @@ const RangeTimeline = React.forwardRef(
73
127
  index
74
128
  )) })
75
129
  ] }),
76
- /* @__PURE__ */ jsxs("div", { className: "ui-range-timeline-body", children: [
130
+ /* @__PURE__ */ jsxs("div", { className: "ui-range-timeline-body", role: nested ? "list" : void 0, children: [
77
131
  (bordered || columns.some((column) => column.muted)) && // Decorative: the same unit tracks as the header columns, laid once behind every row so
78
132
  // each vertical rule runs the full body height exactly under its header column.
79
133
  /* @__PURE__ */ jsxs("div", { className: "ui-range-timeline-grid", "aria-hidden": "true", children: [
@@ -88,103 +142,148 @@ const RangeTimeline = React.forwardRef(
88
142
  index
89
143
  )) })
90
144
  ] }),
91
- rows.map((row) => {
145
+ visibleRows.map((row, index) => {
146
+ const idBase = `${reactId}-row-${rows.indexOf(row)}`;
92
147
  const delta = preview?.id === row.id ? preview.delta : 0;
93
148
  const start = Math.min(row.end, row.start + (preview?.edge === "start" ? delta : 0));
94
149
  const end = Math.max(row.start, row.end + (preview?.edge === "end" ? delta : 0));
95
150
  const left = Math.max(0, start), right = Math.min(units, end + 1);
96
- return /* @__PURE__ */ jsxs("div", { className: "ui-range-timeline-row", children: [
97
- /* @__PURE__ */ jsx("div", { className: "ui-range-timeline-label", children: row.label }),
98
- /* @__PURE__ */ jsxs("div", { className: "ui-range-timeline-track", children: [
99
- today != null && today >= 0 && today < units && /* @__PURE__ */ jsx(
100
- "span",
101
- {
102
- "aria-hidden": "true",
103
- className: "ui-range-timeline-today",
104
- style: { insetInlineStart: `${(today + 0.5) / units * 100}%` }
105
- }
106
- ),
107
- right <= left && (end < 0 ? /* @__PURE__ */ jsxs("span", { className: "ui-range-timeline-outside", "data-direction": "before", children: [
108
- /* @__PURE__ */ jsx(
109
- ChevronLeft,
110
- {
111
- "aria-hidden": "true",
112
- className: "ui-range-timeline-outside-icon"
113
- }
114
- ),
115
- /* @__PURE__ */ jsx(Text, { size: "xs", tone: "muted", children: t("rangeTimeline.outsideBefore") })
116
- ] }) : /* @__PURE__ */ jsxs("span", { className: "ui-range-timeline-outside", "data-direction": "after", children: [
117
- /* @__PURE__ */ jsx(Text, { size: "xs", tone: "muted", children: t("rangeTimeline.outsideAfter") }),
118
- /* @__PURE__ */ jsx(
119
- 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",
120
164
  {
121
- "aria-hidden": "true",
122
- 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
+ ]
123
192
  }
124
- )
125
- ] })),
126
- right > left && /* @__PURE__ */ jsx(
127
- "div",
128
- {
129
- className: "ui-range-timeline-bar",
130
- style: {
131
- insetInlineStart: `${left / units * 100}%`,
132
- inlineSize: `${(right - left) / units * 100}%`
133
- },
134
- children: onRangeChange && (right - left) * Math.max(1, columns.length) >= units && ["start", "end"].filter(
135
- (edge) => edge === "start" ? start >= 0 && start < units : end >= 0 && end < units
136
- ).map((edge) => /* @__PURE__ */ jsx(
137
- 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",
138
197
  {
139
- variant: "ghost",
140
- size: "icon-xs",
141
- className: "ui-range-timeline-handle",
142
- "data-edge": edge,
143
- "aria-label": edge === "start" ? row.startLabel : row.endLabel,
144
- onPointerDown: (event) => {
145
- const width = track.current?.getBoundingClientRect().width ?? 0;
146
- if (!width || event.button !== 0) return;
147
- event.currentTarget.setPointerCapture(event.pointerId);
148
- drag.current = {
149
- id: row.id,
150
- edge,
151
- x: event.clientX,
152
- width,
153
- direction: getComputedStyle(event.currentTarget).direction === "rtl" ? -1 : 1
154
- };
155
- },
156
- onPointerMove: (event) => {
157
- if (drag.current)
158
- setPreview({
159
- id: drag.current.id,
160
- edge: drag.current.edge,
161
- delta: deltaAt(event.clientX)
162
- });
163
- },
164
- onPointerUp: complete,
165
- onPointerCancel: cancel,
166
- onLostPointerCapture: cancel,
167
- onKeyDown: (event) => {
168
- if (event.key === "Escape") {
169
- cancel();
170
- return;
171
- }
172
- const step = event.key === "ArrowRight" ? 1 : event.key === "ArrowLeft" ? -1 : 0;
173
- if (!step) return;
174
- event.preventDefault();
175
- const direction = getComputedStyle(event.currentTarget).direction === "rtl" ? -1 : 1;
176
- const delta2 = step * direction;
177
- if (edge === "start" ? row.start + delta2 <= row.end : row.end + delta2 >= row.start)
178
- 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}%`
179
229
  },
180
- children: /* @__PURE__ */ jsx(GripVertical, { "aria-hidden": "true" })
181
- },
182
- edge
183
- ))
184
- }
185
- )
186
- ] })
187
- ] }, 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
+ );
188
287
  })
189
288
  ] })
190
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.6.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"
@@ -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
  },
@@ -1454,6 +1454,36 @@
1454
1454
  inset-block: 0;
1455
1455
  border-inline-start: 1px solid hsl(var(--primary));
1456
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
+ }
1457
1487
  }
1458
1488
 
1459
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
@@ -293,6 +293,47 @@ export default function Demo() {
293
293
  />
294
294
  </CardContent>
295
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>
296
337
  </Flex>
297
338
  </PageContainer>
298
339
  );
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@godxjp/ui",
3
- "version": "27.6.0",
4
- "godxUiMcp": "27.6.0",
3
+ "version": "27.7.0",
4
+ "godxUiMcp": "27.7.0",
5
5
  "license": "Apache-2.0",
6
6
  "repository": {
7
7
  "type": "git",