@godxjp/ui 27.6.0 → 27.8.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
  ]
@@ -13,6 +13,8 @@ export type { TopbarItemProp, TopbarItemProp as TopbarItemProps, } from "../../p
13
13
  export declare const TopbarItem: React.ForwardRefExoticComponent<Omit<React.ButtonHTMLAttributes<HTMLButtonElement>, "children"> & {
14
14
  hideBelow?: import("../../props/index.js").BreakpointProp;
15
15
  icon?: React.ReactNode;
16
+ iconHideFrom?: import("../../props/index.js").BreakpointProp;
17
+ labelHideBelow?: import("../../props/index.js").BreakpointProp;
16
18
  badge?: React.ReactNode;
17
19
  badgeTone?: import("../../props/components/layout.prop.js").SidebarBadgeToneProp;
18
20
  asChild?: boolean;
@@ -2,10 +2,40 @@ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
2
2
  import * as React from "react";
3
3
  import { Slot } from "../../lib/slot.js";
4
4
  import { cn } from "../../lib/utils.js";
5
- const TopbarItem = React.forwardRef(function TopbarItem2({ asChild = false, className, type, hideBelow, icon, badge, badgeTone, children, ...props }, ref) {
5
+ const TopbarItem = React.forwardRef(function TopbarItem2({
6
+ asChild = false,
7
+ className,
8
+ type,
9
+ hideBelow,
10
+ icon,
11
+ iconHideFrom,
12
+ labelHideBelow,
13
+ badge,
14
+ badgeTone,
15
+ children,
16
+ ...props
17
+ }, ref) {
6
18
  const Comp = asChild ? Slot : "button";
7
19
  const showBadge = !asChild && badge !== void 0 && badge !== "";
8
- const iconNode = !asChild && icon !== void 0 && icon !== null && icon !== false ? /* @__PURE__ */ jsx("span", { "data-slot": "topbar-item-icon", className: "ui-topbar-item-icon", children: icon }) : null;
20
+ const iconNode = icon !== void 0 && icon !== null && icon !== false ? /* @__PURE__ */ jsx(
21
+ "span",
22
+ {
23
+ "data-slot": "topbar-item-icon",
24
+ className: "ui-topbar-item-icon",
25
+ "data-hide-from": iconHideFrom,
26
+ children: icon
27
+ }
28
+ ) : null;
29
+ const labelOf = (label) => labelHideBelow === void 0 || label === void 0 || label === null || label === false ? label : /* @__PURE__ */ jsx(
30
+ "span",
31
+ {
32
+ "data-slot": "topbar-item-label",
33
+ className: "ui-topbar-item-label",
34
+ "data-hide-below": labelHideBelow,
35
+ children: label
36
+ }
37
+ );
38
+ const slotted = asChild && (iconNode !== null || labelHideBelow !== void 0) && React.isValidElement(children) ? React.cloneElement(children, void 0, iconNode, labelOf(children.props.children)) : children;
9
39
  const badgeNode = showBadge ? /* @__PURE__ */ jsx(
10
40
  "span",
11
41
  {
@@ -24,9 +54,9 @@ const TopbarItem = React.forwardRef(function TopbarItem2({ asChild = false, clas
24
54
  className: cn("ui-topbar-item ui-focus-ring", className),
25
55
  type: asChild ? void 0 : type ?? "button",
26
56
  ...props,
27
- children: asChild ? children : /* @__PURE__ */ jsxs(Fragment, { children: [
57
+ children: asChild ? slotted : /* @__PURE__ */ jsxs(Fragment, { children: [
28
58
  iconNode,
29
- children,
59
+ labelOf(children),
30
60
  badgeNode
31
61
  ] })
32
62
  }
@@ -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.8.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"
@@ -1354,8 +1354,24 @@ export type TopbarItemProp = Omit<React.ButtonHTMLAttributes<HTMLButtonElement>,
1354
1354
  * visible label, not from the glyph. For a glyph that must carry a name, or one that needs a
1355
1355
  * step other than the bar's, pass `<Icon as={…} size=… label=… />` — as this prop's value or as
1356
1356
  * a child — and it keeps its own metric and its own semantics.
1357
+ *
1358
+ * Rendered under `asChild` too: the slot is inserted ahead of the child element's own children,
1359
+ * and the child element stays the cell (gh#726).
1357
1360
  */
1358
1361
  icon?: ReactNode;
1362
+ /**
1363
+ * Drop the `icon` slot FROM a breakpoint step upwards — `Flex hideFrom`'s contract, scoped to
1364
+ * the cell's glyph, so a cell reads label-only on a wide bar without the consumer wrapping the
1365
+ * glyph (gh#726). Purely decorative: the accessible name is unaffected.
1366
+ */
1367
+ iconHideFrom?: BreakpointProp;
1368
+ /**
1369
+ * Visually hide the cell's label BELOW a breakpoint step — `Flex hideBelow`'s contract, scoped to
1370
+ * the label — so the cell collapses to icon-only on a narrow bar (gh#726). The label stays in the
1371
+ * accessibility tree (visually hidden, not `display: none`), so the collapsed cell keeps its
1372
+ * accessible name. Under `asChild` the child element's own children are the label.
1373
+ */
1374
+ labelHideBelow?: BreakpointProp;
1359
1375
  /**
1360
1376
  * Unread count OVERLAID on the cell's glyph — the notification-bell affordance the cell's own use
1361
1377
  * cases name (gh#398). Pass the CONTENT ONLY, exactly like `SidebarItemProp.badge`: a number, a
@@ -1187,6 +1187,14 @@ export declare const COMPONENT_PROP_REGISTRY: {
1187
1187
  readonly field: "asChild";
1188
1188
  readonly local: true;
1189
1189
  readonly reason: "Radix Slot passthrough — merges onto the child";
1190
+ }, "BreakpointProp", {
1191
+ readonly field: "iconHideFrom";
1192
+ readonly local: true;
1193
+ readonly reason: "Flex `hideFrom` scoped to the cell's icon slot — label-only from a breakpoint up without hand-wrapping the glyph (gh#726).";
1194
+ }, {
1195
+ readonly field: "labelHideBelow";
1196
+ readonly local: true;
1197
+ readonly reason: "Flex `hideBelow` scoped to the cell's label — icon-only below a breakpoint; visually hidden so the accessible name survives (gh#726).";
1190
1198
  }];
1191
1199
  };
1192
1200
  readonly NavListProp: {
@@ -2950,7 +2958,7 @@ export declare const COMPONENT_PROP_REGISTRY: {
2950
2958
  }, {
2951
2959
  readonly field: "rows";
2952
2960
  readonly local: true;
2953
- readonly reason: "Consumer-supplied interval records preserve true inclusive endpoints independently of clipping.";
2961
+ 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
2962
  }, {
2955
2963
  readonly field: "today";
2956
2964
  readonly local: true;
@@ -2959,6 +2967,18 @@ export declare const COMPONENT_PROP_REGISTRY: {
2959
2967
  readonly field: "onRangeChange";
2960
2968
  readonly local: true;
2961
2969
  readonly reason: "An endpoint movement command, not an internally owned value; the consumer commits its row data.";
2970
+ }, {
2971
+ readonly field: "expandedValues";
2972
+ readonly local: true;
2973
+ readonly reason: "Controlled expanded parent row ids (gh#724) — the same spelling as `Tree expandedValues` (antd `expandedKeys` / Table `expandedRowKeys`), per docs/DESIGN-AUTHORITY.md.";
2974
+ }, {
2975
+ readonly field: "defaultExpandedValues";
2976
+ readonly local: true;
2977
+ readonly reason: "Uncontrolled initial expanded parents, same spelling as `Tree`; omitted, every parent starts expanded so adding `depth` never hides a row.";
2978
+ }, {
2979
+ readonly field: "onExpandedValuesChange";
2980
+ readonly local: true;
2981
+ readonly reason: "Next expanded parent ids, fired for controlled and uncontrolled timelines alike — the `Tree onExpandedValuesChange` callback.";
2962
2982
  }];
2963
2983
  };
2964
2984
  readonly ResponsiveGridProp: {
@@ -1284,7 +1284,18 @@ const COMPONENT_PROP_REGISTRY = {
1284
1284
  vocabulary: [
1285
1285
  "ChildrenProp",
1286
1286
  "ClassNameProp",
1287
- { field: "asChild", local: true, reason: "Radix Slot passthrough \u2014 merges onto the child" }
1287
+ { field: "asChild", local: true, reason: "Radix Slot passthrough \u2014 merges onto the child" },
1288
+ "BreakpointProp",
1289
+ {
1290
+ field: "iconHideFrom",
1291
+ local: true,
1292
+ reason: "Flex `hideFrom` scoped to the cell's icon slot \u2014 label-only from a breakpoint up without hand-wrapping the glyph (gh#726)."
1293
+ },
1294
+ {
1295
+ field: "labelHideBelow",
1296
+ local: true,
1297
+ reason: "Flex `hideBelow` scoped to the cell's label \u2014 icon-only below a breakpoint; visually hidden so the accessible name survives (gh#726)."
1298
+ }
1288
1299
  ]
1289
1300
  },
1290
1301
  NavListProp: {
@@ -3460,7 +3471,7 @@ const COMPONENT_PROP_REGISTRY = {
3460
3471
  {
3461
3472
  field: "rows",
3462
3473
  local: true,
3463
- reason: "Consumer-supplied interval records preserve true inclusive endpoints independently of clipping."
3474
+ 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
3475
  },
3465
3476
  {
3466
3477
  field: "today",
@@ -3471,6 +3482,21 @@ const COMPONENT_PROP_REGISTRY = {
3471
3482
  field: "onRangeChange",
3472
3483
  local: true,
3473
3484
  reason: "An endpoint movement command, not an internally owned value; the consumer commits its row data."
3485
+ },
3486
+ {
3487
+ field: "expandedValues",
3488
+ local: true,
3489
+ reason: "Controlled expanded parent row ids (gh#724) \u2014 the same spelling as `Tree expandedValues` (antd `expandedKeys` / Table `expandedRowKeys`), per docs/DESIGN-AUTHORITY.md."
3490
+ },
3491
+ {
3492
+ field: "defaultExpandedValues",
3493
+ local: true,
3494
+ reason: "Uncontrolled initial expanded parents, same spelling as `Tree`; omitted, every parent starts expanded so adding `depth` never hides a row."
3495
+ },
3496
+ {
3497
+ field: "onExpandedValuesChange",
3498
+ local: true,
3499
+ reason: "Next expanded parent ids, fired for controlled and uncontrolled timelines alike \u2014 the `Tree onExpandedValuesChange` callback."
3474
3500
  }
3475
3501
  ]
3476
3502
  },
@@ -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 {
@@ -1485,28 +1485,84 @@
1485
1485
  }
1486
1486
 
1487
1487
  @media (width >= 40rem) {
1488
- .ui-flex[data-hide-from="sm"] {
1488
+ .ui-flex[data-hide-from="sm"],
1489
+ .ui-topbar-item-icon[data-hide-from="sm"] {
1489
1490
  display: none;
1490
1491
  }
1491
1492
  }
1492
1493
 
1493
1494
  @media (width >= 48rem) {
1494
- .ui-flex[data-hide-from="md"] {
1495
+ .ui-flex[data-hide-from="md"],
1496
+ .ui-topbar-item-icon[data-hide-from="md"] {
1495
1497
  display: none;
1496
1498
  }
1497
1499
  }
1498
1500
 
1499
1501
  @media (width >= 64rem) {
1500
- .ui-flex[data-hide-from="lg"] {
1502
+ .ui-flex[data-hide-from="lg"],
1503
+ .ui-topbar-item-icon[data-hide-from="lg"] {
1501
1504
  display: none;
1502
1505
  }
1503
1506
  }
1504
1507
 
1505
1508
  @media (width >= 80rem) {
1506
- .ui-flex[data-hide-from="xl"] {
1509
+ .ui-flex[data-hide-from="xl"],
1510
+ .ui-topbar-item-icon[data-hide-from="xl"] {
1507
1511
  display: none;
1508
1512
  }
1509
1513
  }
1514
+
1515
+ @media (width < 40rem) {
1516
+ .ui-topbar-item-label[data-hide-below="sm"] {
1517
+ position: absolute;
1518
+ inset-block-start: 0;
1519
+ inset-inline-start: 0;
1520
+ inline-size: var(--stroke-hairline);
1521
+ block-size: var(--stroke-hairline);
1522
+ overflow: hidden;
1523
+ clip-path: inset(50%);
1524
+ white-space: nowrap;
1525
+ }
1526
+ }
1527
+
1528
+ @media (width < 48rem) {
1529
+ .ui-topbar-item-label[data-hide-below="md"] {
1530
+ position: absolute;
1531
+ inset-block-start: 0;
1532
+ inset-inline-start: 0;
1533
+ inline-size: var(--stroke-hairline);
1534
+ block-size: var(--stroke-hairline);
1535
+ overflow: hidden;
1536
+ clip-path: inset(50%);
1537
+ white-space: nowrap;
1538
+ }
1539
+ }
1540
+
1541
+ @media (width < 64rem) {
1542
+ .ui-topbar-item-label[data-hide-below="lg"] {
1543
+ position: absolute;
1544
+ inset-block-start: 0;
1545
+ inset-inline-start: 0;
1546
+ inline-size: var(--stroke-hairline);
1547
+ block-size: var(--stroke-hairline);
1548
+ overflow: hidden;
1549
+ clip-path: inset(50%);
1550
+ white-space: nowrap;
1551
+ }
1552
+ }
1553
+
1554
+ @media (width < 80rem) {
1555
+ .ui-topbar-item-label[data-hide-below="xl"] {
1556
+ position: absolute;
1557
+ inset-block-start: 0;
1558
+ inset-inline-start: 0;
1559
+ inline-size: var(--stroke-hairline);
1560
+ block-size: var(--stroke-hairline);
1561
+ overflow: hidden;
1562
+ clip-path: inset(50%);
1563
+ white-space: nowrap;
1564
+ }
1565
+ }
1510
1566
  }
1511
1567
 
1512
1568
  @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
  );
@@ -29,6 +29,7 @@ import {
29
29
  PanelLeftClose,
30
30
  PanelLeftOpen,
31
31
  Settings,
32
+ Target,
32
33
  Users,
33
34
  } from "lucide-react";
34
35
 
@@ -178,6 +179,42 @@ export default function Demo() {
178
179
  />
179
180
  </CardContent>
180
181
  </Card>
182
+
183
+ <Card>
184
+ <CardHeader>
185
+ <CardTitle level={2}>リンクのセルと、幅で畳むセル</CardTitle>
186
+ <CardDescription>
187
+ `asChild` でもセルは `icon`
188
+ スロットを子要素の中に差し込み、リンクそのものがセルです。 `labelHideBelow`
189
+ はその幅未満でラベルを視覚的にだけ隠すので、アイコンだけになっても
190
+ ラベルがアクセシブルネームとして残ります。`iconHideFrom`
191
+ はその幅以上でアイコンを外します。 ビューポートを 640px
192
+ の前後に動かして確認してください。
193
+ </CardDescription>
194
+ </CardHeader>
195
+ <CardContent>
196
+ <Topbar
197
+ start={
198
+ <>
199
+ <TopbarItem asChild icon={<Target aria-hidden="true" />}>
200
+ <a href="#topbar-item-goals">目標</a>
201
+ </TopbarItem>
202
+ <TopbarItem
203
+ asChild
204
+ icon={<Target aria-hidden="true" />}
205
+ labelHideBelow="sm"
206
+ iconHideFrom="sm"
207
+ >
208
+ <a href="#topbar-item-goals-responsive">今期の目標</a>
209
+ </TopbarItem>
210
+ <TopbarItem icon={<Building2 aria-hidden="true" />} labelHideBelow="md">
211
+ 東京本社
212
+ </TopbarItem>
213
+ </>
214
+ }
215
+ />
216
+ </CardContent>
217
+ </Card>
181
218
  </Flex>
182
219
  </PageContainer>
183
220
  </AppShell>
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.8.0",
4
+ "godxUiMcp": "27.8.0",
5
5
  "license": "Apache-2.0",
6
6
  "repository": {
7
7
  "type": "git",