@godxjp/ui 23.2.1 → 23.4.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.
- package/dist/app/timezones.js +2 -1
- package/dist/components/charts/chart-cartesian.js +2 -4
- package/dist/components/charts/chart-frame.js +2 -1
- package/dist/components/data-display/card.d.ts +87 -1
- package/dist/components/data-display/card.js +88 -15
- package/dist/components/data-display/index.d.ts +5 -1
- package/dist/components/data-display/index.js +5 -0
- package/dist/components/data-display/thought-chain.d.ts +42 -0
- package/dist/components/data-display/thought-chain.js +177 -0
- package/dist/components/data-display/welcome.d.ts +26 -0
- package/dist/components/data-display/welcome.js +37 -0
- package/dist/components/data-entry/attachments.d.ts +19 -0
- package/dist/components/data-entry/attachments.js +322 -0
- package/dist/components/data-entry/date-picker.js +12 -14
- package/dist/components/data-entry/index.d.ts +2 -0
- package/dist/components/data-entry/index.js +2 -0
- package/dist/components/data-entry/number-input.js +2 -1
- package/dist/components/feedback/dialog.d.ts +10 -7
- package/dist/components/feedback/dialog.js +40 -17
- package/dist/components/feedback/index.d.ts +3 -3
- package/dist/components/feedback/index.js +2 -0
- package/dist/components/feedback/skeleton.d.ts +11 -2
- package/dist/components/feedback/skeleton.js +8 -0
- package/dist/components/general/actions.d.ts +59 -0
- package/dist/components/general/actions.js +253 -0
- package/dist/components/general/activity.js +1 -0
- package/dist/components/general/button.js +2 -1
- package/dist/components/general/float-button.d.ts +41 -0
- package/dist/components/general/float-button.js +336 -0
- package/dist/components/general/index.d.ts +6 -2
- package/dist/components/general/index.js +12 -1
- package/dist/components/general/typography.d.ts +79 -4
- package/dist/components/general/typography.js +598 -54
- package/dist/components/layout/draggable-panel.d.ts +18 -0
- package/dist/components/layout/draggable-panel.js +189 -0
- package/dist/components/layout/error-surface.js +3 -2
- package/dist/components/layout/index.d.ts +2 -0
- package/dist/components/layout/index.js +2 -0
- package/dist/components/navigation/conversations.d.ts +39 -0
- package/dist/components/navigation/conversations.js +371 -0
- package/dist/components/navigation/index.d.ts +2 -0
- package/dist/components/navigation/index.js +2 -0
- package/dist/components/ui/card.js +1 -0
- package/dist/components/ui/toggle.js +2 -1
- package/dist/i18n/messages/en.json +53 -0
- package/dist/i18n/messages/ja.json +53 -0
- package/dist/i18n/messages/vi.json +53 -0
- package/dist/i18n/translate.js +3 -2
- package/dist/lib/datetime/picker-format.js +2 -1
- package/dist/lib/format.js +3 -2
- package/dist/lib/intl-cache.d.ts +32 -0
- package/dist/lib/intl-cache.js +29 -0
- package/dist/props/components/data-display.prop.d.ts +143 -1
- package/dist/props/components/data-entry.prop.d.ts +87 -0
- package/dist/props/components/feedback.prop.d.ts +53 -1
- package/dist/props/components/general.prop.d.ts +440 -3
- package/dist/props/components/index.d.ts +2 -2
- package/dist/props/components/layout.prop.d.ts +63 -1
- package/dist/props/components/navigation.prop.d.ts +150 -0
- package/dist/props/registry.d.ts +431 -0
- package/dist/props/registry.js +551 -0
- package/dist/props/vocabulary/index.d.ts +1 -1
- package/dist/props/vocabulary/interaction.prop.d.ts +113 -0
- package/dist/styles/alert-layout.css +22 -0
- package/dist/styles/card-layout.css +34 -0
- package/dist/styles/core.css +1 -0
- package/dist/styles/data-display-layout.css +236 -0
- package/dist/styles/float-button-layout.css +168 -0
- package/dist/styles/form-layout.css +11 -1
- package/dist/styles/index.css +1 -0
- package/dist/styles/layout.css +196 -0
- package/dist/styles/navigation-layout.css +139 -0
- package/dist/styles/text-layout.css +156 -0
- package/dist/tokens/axes.css +5 -0
- package/dist/tokens/base.css +7 -0
- package/dist/tokens/components/actions.css +14 -0
- package/dist/tokens/components/attachments.css +15 -0
- package/dist/tokens/components/conversations.css +28 -0
- package/dist/tokens/components/draggable-panel.css +25 -0
- package/dist/tokens/components/float-button.css +39 -0
- package/dist/tokens/components/text.css +13 -0
- package/dist/tokens/components/thought-chain.css +31 -0
- package/dist/tokens/components/welcome.css +19 -0
- package/dist/tokens/foundation.css +6 -4
- package/docs/DESIGN-AUTHORITY.md +133 -20
- package/docs/FRAME-COVERAGE-REPORT.md +5 -2
- package/docs/data-display/card/examples/tab-list.tsx +97 -0
- package/docs/data-display/thought-chain.tsx +176 -0
- package/docs/data-display/welcome.tsx +115 -0
- package/docs/data-entry/attachments.tsx +160 -0
- package/docs/feedback/dialog.tsx +50 -0
- package/docs/general/actions.tsx +175 -0
- package/docs/general/float-button.tsx +133 -0
- package/docs/general/typography.tsx +63 -1
- package/docs/layout/draggable-panel.tsx +127 -0
- package/docs/navigation/conversations.tsx +176 -0
- package/docs/roadmap/parity-audit-layout-navigation-general.md +17 -16
- package/package.json +20 -4
- package/scripts/_agent-setup.mjs +36 -5
- package/scripts/guinea-pig-skill.md +14 -0
- package/scripts/ui-audit.mjs +69 -9
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
import type { DragAxisProp, DraggablePanelPositionProp } from "../../props/components/layout.prop.js";
|
|
3
|
+
export type { DraggablePanelProp, DraggablePanelProp as DraggablePanelProps, DraggablePanelPlacementProp, DraggablePanelPositionProp, DragAxisProp, DragBoundsProp, } from "../../props/components/layout.prop.js";
|
|
4
|
+
export declare const DraggablePanel: React.ForwardRefExoticComponent<Omit<React.HTMLAttributes<HTMLElement>, "children" | "onDrag" | "title"> & {
|
|
5
|
+
title: import("../../props/index.js").TitleProp;
|
|
6
|
+
children?: import("../../props/index.js").ChildrenProp;
|
|
7
|
+
extra?: import("../../props/index.js").ExtraProp;
|
|
8
|
+
placement?: import("./draggable-panel.js").DraggablePanelPlacementProp;
|
|
9
|
+
width?: Extract<import("../../props/index.js").SizeProp, "sm" | "md" | "lg">;
|
|
10
|
+
axis?: DragAxisProp;
|
|
11
|
+
bounds?: import("./draggable-panel.js").DragBoundsProp;
|
|
12
|
+
position?: DraggablePanelPositionProp;
|
|
13
|
+
defaultPosition?: DraggablePanelPositionProp;
|
|
14
|
+
onPositionChange?: (position: DraggablePanelPositionProp) => void;
|
|
15
|
+
onClose?: () => void;
|
|
16
|
+
disabled?: import("../../props/index.js").DisabledProp;
|
|
17
|
+
className?: import("../../props/index.js").ClassNameProp;
|
|
18
|
+
} & React.RefAttributes<HTMLElement>>;
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
3
|
+
import * as React from "react";
|
|
4
|
+
import { GripVertical, X } from "lucide-react";
|
|
5
|
+
import { cn } from "../../lib/utils.js";
|
|
6
|
+
import { useTranslation } from "../../i18n/use-translation.js";
|
|
7
|
+
const AXIS_ALLOWS_X = {
|
|
8
|
+
both: true,
|
|
9
|
+
x: true,
|
|
10
|
+
y: false,
|
|
11
|
+
none: false
|
|
12
|
+
};
|
|
13
|
+
const AXIS_ALLOWS_Y = {
|
|
14
|
+
both: true,
|
|
15
|
+
x: false,
|
|
16
|
+
y: true,
|
|
17
|
+
none: false
|
|
18
|
+
};
|
|
19
|
+
function readStep(element, name, fallback) {
|
|
20
|
+
if (!element || typeof window === "undefined" || typeof window.getComputedStyle !== "function") {
|
|
21
|
+
return fallback;
|
|
22
|
+
}
|
|
23
|
+
const raw = window.getComputedStyle(element).getPropertyValue(name).trim();
|
|
24
|
+
const parsed = Number.parseFloat(raw);
|
|
25
|
+
return Number.isFinite(parsed) && parsed > 0 ? parsed : fallback;
|
|
26
|
+
}
|
|
27
|
+
function clampToViewport(element, next, current) {
|
|
28
|
+
if (!element || typeof window === "undefined") return next;
|
|
29
|
+
const rect = element.getBoundingClientRect();
|
|
30
|
+
const restLeft = rect.left - current.x;
|
|
31
|
+
const restTop = rect.top - current.y;
|
|
32
|
+
const maxLeft = window.innerWidth - rect.width;
|
|
33
|
+
const maxTop = window.innerHeight - rect.height;
|
|
34
|
+
const clamp = (value, min, max) => max < min ? min : Math.min(Math.max(value, min), max);
|
|
35
|
+
return {
|
|
36
|
+
x: clamp(next.x, -restLeft, maxLeft - restLeft),
|
|
37
|
+
y: clamp(next.y, -restTop, maxTop - restTop)
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
const ORIGIN = { x: 0, y: 0 };
|
|
41
|
+
const DraggablePanel = React.forwardRef(
|
|
42
|
+
({
|
|
43
|
+
title,
|
|
44
|
+
children,
|
|
45
|
+
extra,
|
|
46
|
+
placement = "bottom-end",
|
|
47
|
+
width = "md",
|
|
48
|
+
axis = "both",
|
|
49
|
+
bounds = "viewport",
|
|
50
|
+
position,
|
|
51
|
+
defaultPosition,
|
|
52
|
+
onPositionChange,
|
|
53
|
+
onClose,
|
|
54
|
+
disabled = false,
|
|
55
|
+
className,
|
|
56
|
+
...props
|
|
57
|
+
}, ref) => {
|
|
58
|
+
const { t } = useTranslation();
|
|
59
|
+
const panelRef = React.useRef(null);
|
|
60
|
+
const [uncontrolled, setUncontrolled] = React.useState(defaultPosition ?? ORIGIN);
|
|
61
|
+
const [dragging, setDragging] = React.useState(false);
|
|
62
|
+
const stopDrag = React.useRef(null);
|
|
63
|
+
const current = position ?? uncontrolled;
|
|
64
|
+
const currentRef = React.useRef(current);
|
|
65
|
+
currentRef.current = current;
|
|
66
|
+
const commit = React.useCallback(
|
|
67
|
+
(next) => {
|
|
68
|
+
const clamped = bounds === "viewport" ? clampToViewport(panelRef.current, next, currentRef.current) : next;
|
|
69
|
+
if (clamped.x === currentRef.current.x && clamped.y === currentRef.current.y) return;
|
|
70
|
+
if (position === void 0) setUncontrolled(clamped);
|
|
71
|
+
onPositionChange?.(clamped);
|
|
72
|
+
},
|
|
73
|
+
[bounds, onPositionChange, position]
|
|
74
|
+
);
|
|
75
|
+
React.useEffect(() => () => stopDrag.current?.(), []);
|
|
76
|
+
const setPanel = React.useCallback(
|
|
77
|
+
(node) => {
|
|
78
|
+
panelRef.current = node;
|
|
79
|
+
if (typeof ref === "function") ref(node);
|
|
80
|
+
else if (ref) ref.current = node;
|
|
81
|
+
},
|
|
82
|
+
[ref]
|
|
83
|
+
);
|
|
84
|
+
const handlePointerDown = (event) => {
|
|
85
|
+
if (disabled || axis === "none") return;
|
|
86
|
+
if (event.button !== 0 || event.ctrlKey || event.metaKey || event.altKey) return;
|
|
87
|
+
event.preventDefault();
|
|
88
|
+
const pointerId = event.pointerId;
|
|
89
|
+
const origin = { x: event.clientX, y: event.clientY };
|
|
90
|
+
const start = currentRef.current;
|
|
91
|
+
const onMove = (move) => {
|
|
92
|
+
if (move.pointerId !== pointerId) return;
|
|
93
|
+
commit({
|
|
94
|
+
x: AXIS_ALLOWS_X[axis] ? start.x + (move.clientX - origin.x) : start.x,
|
|
95
|
+
y: AXIS_ALLOWS_Y[axis] ? start.y + (move.clientY - origin.y) : start.y
|
|
96
|
+
});
|
|
97
|
+
};
|
|
98
|
+
const finish = () => {
|
|
99
|
+
window.removeEventListener("pointermove", onMove);
|
|
100
|
+
window.removeEventListener("pointerup", onUp);
|
|
101
|
+
window.removeEventListener("pointercancel", onUp);
|
|
102
|
+
stopDrag.current = null;
|
|
103
|
+
setDragging(false);
|
|
104
|
+
};
|
|
105
|
+
const onUp = (up) => {
|
|
106
|
+
if (up.pointerId !== pointerId) return;
|
|
107
|
+
finish();
|
|
108
|
+
};
|
|
109
|
+
stopDrag.current?.();
|
|
110
|
+
stopDrag.current = finish;
|
|
111
|
+
setDragging(true);
|
|
112
|
+
window.addEventListener("pointermove", onMove);
|
|
113
|
+
window.addEventListener("pointerup", onUp);
|
|
114
|
+
window.addEventListener("pointercancel", onUp);
|
|
115
|
+
};
|
|
116
|
+
const handleKeyDown = (event) => {
|
|
117
|
+
if (disabled || axis === "none") return;
|
|
118
|
+
const horizontal = event.key === "ArrowLeft" || event.key === "ArrowRight";
|
|
119
|
+
const vertical = event.key === "ArrowUp" || event.key === "ArrowDown";
|
|
120
|
+
if (!horizontal && !vertical) return;
|
|
121
|
+
if (horizontal && !AXIS_ALLOWS_X[axis]) return;
|
|
122
|
+
if (vertical && !AXIS_ALLOWS_Y[axis]) return;
|
|
123
|
+
event.preventDefault();
|
|
124
|
+
const step = readStep(
|
|
125
|
+
panelRef.current,
|
|
126
|
+
event.shiftKey ? "--draggable-panel-step-offset-lg" : "--draggable-panel-step-offset",
|
|
127
|
+
event.shiftKey ? 40 : 8
|
|
128
|
+
);
|
|
129
|
+
const sign = event.key === "ArrowLeft" || event.key === "ArrowUp" ? -1 : 1;
|
|
130
|
+
commit({
|
|
131
|
+
x: horizontal ? currentRef.current.x + sign * step : currentRef.current.x,
|
|
132
|
+
y: vertical ? currentRef.current.y + sign * step : currentRef.current.y
|
|
133
|
+
});
|
|
134
|
+
};
|
|
135
|
+
const movable = !disabled && axis !== "none";
|
|
136
|
+
return /* @__PURE__ */ jsxs(
|
|
137
|
+
"section",
|
|
138
|
+
{
|
|
139
|
+
ref: setPanel,
|
|
140
|
+
"data-slot": "draggable-panel",
|
|
141
|
+
"data-placement": placement,
|
|
142
|
+
"data-width": width,
|
|
143
|
+
"data-dragging": dragging ? "" : void 0,
|
|
144
|
+
"aria-label": typeof title === "string" ? title : void 0,
|
|
145
|
+
className: cn("ui-draggable-panel", className),
|
|
146
|
+
style: {
|
|
147
|
+
"--draggable-panel-offset-x": current.x,
|
|
148
|
+
"--draggable-panel-offset-y": current.y
|
|
149
|
+
},
|
|
150
|
+
...props,
|
|
151
|
+
children: [
|
|
152
|
+
/* @__PURE__ */ jsxs("div", { className: "ui-draggable-panel-bar", children: [
|
|
153
|
+
/* @__PURE__ */ jsx(
|
|
154
|
+
"button",
|
|
155
|
+
{
|
|
156
|
+
type: "button",
|
|
157
|
+
"data-slot": "draggable-panel-handle",
|
|
158
|
+
className: "ui-draggable-panel-handle ui-focus-ring",
|
|
159
|
+
"aria-label": t("layout.draggablePanel.moveLabel"),
|
|
160
|
+
"aria-disabled": movable ? void 0 : "true",
|
|
161
|
+
onPointerDown: handlePointerDown,
|
|
162
|
+
onKeyDown: handleKeyDown,
|
|
163
|
+
children: /* @__PURE__ */ jsx(GripVertical, { className: "ui-draggable-panel-handle-icon", "aria-hidden": "true" })
|
|
164
|
+
}
|
|
165
|
+
),
|
|
166
|
+
/* @__PURE__ */ jsx("span", { className: "ui-draggable-panel-title", children: title }),
|
|
167
|
+
extra != null && /* @__PURE__ */ jsx("div", { className: "ui-draggable-panel-extra", children: extra }),
|
|
168
|
+
onClose != null && /* @__PURE__ */ jsx(
|
|
169
|
+
"button",
|
|
170
|
+
{
|
|
171
|
+
type: "button",
|
|
172
|
+
"data-slot": "draggable-panel-close",
|
|
173
|
+
className: "ui-draggable-panel-close ui-focus-ring",
|
|
174
|
+
"aria-label": t("feedback.alert.dismiss"),
|
|
175
|
+
onClick: onClose,
|
|
176
|
+
children: /* @__PURE__ */ jsx(X, { className: "ui-draggable-panel-close-icon", "aria-hidden": "true" })
|
|
177
|
+
}
|
|
178
|
+
)
|
|
179
|
+
] }),
|
|
180
|
+
/* @__PURE__ */ jsx("div", { className: "ui-draggable-panel-body", children })
|
|
181
|
+
]
|
|
182
|
+
}
|
|
183
|
+
);
|
|
184
|
+
}
|
|
185
|
+
);
|
|
186
|
+
DraggablePanel.displayName = "DraggablePanel";
|
|
187
|
+
export {
|
|
188
|
+
DraggablePanel
|
|
189
|
+
};
|
|
@@ -8,6 +8,7 @@ import { EmptyState } from "../data-display/empty-state.js";
|
|
|
8
8
|
import { Progress } from "../data-display/progress.js";
|
|
9
9
|
import { Text } from "../general/typography.js";
|
|
10
10
|
import { CenteredShell } from "./centered-shell.js";
|
|
11
|
+
import { dateTimeFormat, numberFormat } from "../../lib/intl-cache.js";
|
|
11
12
|
const STATUS_META = {
|
|
12
13
|
400: { icon: TriangleAlert, tone: "warning" },
|
|
13
14
|
403: { icon: ShieldAlert, tone: "warning" },
|
|
@@ -37,7 +38,7 @@ function formatMaintenanceWindow(maintenance, locale) {
|
|
|
37
38
|
};
|
|
38
39
|
const start = new Date(maintenance.start);
|
|
39
40
|
if (Number.isNaN(start.getTime())) return maintenance.start;
|
|
40
|
-
const formatter =
|
|
41
|
+
const formatter = dateTimeFormat(locale, options);
|
|
41
42
|
if (maintenance.end === void 0) return formatter.format(start);
|
|
42
43
|
const end = new Date(maintenance.end);
|
|
43
44
|
if (Number.isNaN(end.getTime())) return formatter.format(start);
|
|
@@ -72,7 +73,7 @@ const ErrorSurface = React.forwardRef(
|
|
|
72
73
|
const maintenanceProgress = maintenance?.progress;
|
|
73
74
|
const hasProgress = typeof maintenanceProgress === "number" && Number.isFinite(maintenanceProgress);
|
|
74
75
|
const progressLabel = hasProgress ? t("layout.errorSurface.maintenanceProgress", {
|
|
75
|
-
percent:
|
|
76
|
+
percent: numberFormat(locale, {
|
|
76
77
|
style: "percent",
|
|
77
78
|
maximumFractionDigits: 0
|
|
78
79
|
}).format(Math.max(0, Math.min(100, maintenanceProgress)) / 100)
|
|
@@ -49,6 +49,8 @@ export { MasterDetail } from "./master-detail.js";
|
|
|
49
49
|
export type { MasterDetailProps } from "./master-detail.js";
|
|
50
50
|
export { SplitPane } from "./split-pane.js";
|
|
51
51
|
export type { SplitPaneProps } from "./split-pane.js";
|
|
52
|
+
export { DraggablePanel } from "./draggable-panel.js";
|
|
53
|
+
export type { DraggablePanelProp, DraggablePanelProps, DraggablePanelPlacementProp, DraggablePanelPositionProp, DragAxisProp, DragBoundsProp, } from "./draggable-panel.js";
|
|
52
54
|
export { Separator } from "./separator.js";
|
|
53
55
|
export type { SeparatorProp, SeparatorProps } from "./separator.js";
|
|
54
56
|
export { AspectRatio } from "./aspect-ratio.js";
|
|
@@ -22,6 +22,7 @@ import { TopbarItem } from "./topbar-item.js";
|
|
|
22
22
|
import { ResponsiveGrid } from "./responsive-grid.js";
|
|
23
23
|
import { MasterDetail } from "./master-detail.js";
|
|
24
24
|
import { SplitPane } from "./split-pane.js";
|
|
25
|
+
import { DraggablePanel } from "./draggable-panel.js";
|
|
25
26
|
import { Separator } from "./separator.js";
|
|
26
27
|
import { AspectRatio } from "./aspect-ratio.js";
|
|
27
28
|
import { LegalDocumentShell } from "./legal-document-shell.js";
|
|
@@ -40,6 +41,7 @@ export {
|
|
|
40
41
|
AuthStack,
|
|
41
42
|
Breadcrumb,
|
|
42
43
|
CenteredShell,
|
|
44
|
+
DraggablePanel,
|
|
43
45
|
ErrorSurface,
|
|
44
46
|
Flex,
|
|
45
47
|
LegalDocumentShell,
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
import type { ConversationsProp } from "../../props/components/navigation.prop.js";
|
|
3
|
+
export type { ConversationsProp, ConversationsProp as ConversationsProps, ConversationsItemProp, ConversationsDividerProp, ConversationsEntryProp, ConversationsMenuProp, ConversationsMenuItemProp, ConversationsGroupableProp, ConversationsCreationProp, } from "../../props/components/navigation.prop.js";
|
|
4
|
+
/**
|
|
5
|
+
* Conversations — the session rail of a chat surface (Ant Design X `Conversations`).
|
|
6
|
+
*
|
|
7
|
+
* ## What is ported, and the one half that is deliberately not
|
|
8
|
+
*
|
|
9
|
+
* Every prop a caller passes keeps Ant X's spelling and semantics: `items` (conversations and
|
|
10
|
+
* `{ type: "divider" }` rules), `activeKey` / `defaultActiveKey` / `onActiveChange`, `menu` (flat
|
|
11
|
+
* or per-row), `groupable` (with `label`, `collapsible` and the `expandedKeys` triad) and
|
|
12
|
+
* `creation`. The two knobs left out are `styles` / `classNames` — the semantic style maps — and
|
|
13
|
+
* they are left out by a standing decision of this repository, not by omission: a free-form style
|
|
14
|
+
* hole freezes internal DOM slot names into public API and is unmeasurable by the audit
|
|
15
|
+
* (docs/WHAT-BELONGS-HERE.md · "Lỗ kiểu dáng tự do"; docs/DESIGN-AUTHORITY.md · "A knob that only
|
|
16
|
+
* a fork could reach is not parity either"). The rail is retuned through
|
|
17
|
+
* `src/tokens/components/conversations.css` instead. `prefixCls`, `rootClassName`,
|
|
18
|
+
* `menu.getPopupContainer`, `menu.trigger` and `shortcutKeys` are out for the same reason or
|
|
19
|
+
* because the subsystem behind them does not exist here (see the PR for the per-prop ledger).
|
|
20
|
+
*
|
|
21
|
+
* ## What this adds, because Ant X's rail has no keyboard model at all
|
|
22
|
+
*
|
|
23
|
+
* Measured in `@ant-design/x@2.9.0`: `es/conversations/Item.js` renders a bare
|
|
24
|
+
* `<li title … onClick>`. No `role`, no `tabIndex`, no key handling — the rail is unreachable by
|
|
25
|
+
* keyboard, and a screen reader meets twelve list items with no hint that any of them is a
|
|
26
|
+
* control or that one of them is current. Neither is the hand-rolled alternative the issue
|
|
27
|
+
* describes (a stack of `Button`s) any better on the axis that matters: it is one tab stop PER
|
|
28
|
+
* conversation.
|
|
29
|
+
*
|
|
30
|
+
* So the rows here are real `Button`s under ONE roving tabindex, the APG composite-widget rule:
|
|
31
|
+
*
|
|
32
|
+
* - the whole rail — group headings included — is a single tab stop;
|
|
33
|
+
* - `↑` / `↓` walk it and wrap, `Home` / `End` jump to its ends;
|
|
34
|
+
* - the logical FORWARD arrow (`→` in LTR, `←` in RTL) steps from a row onto its overflow menu,
|
|
35
|
+
* and the backward arrow steps back, so a row's second action costs no extra tab stop;
|
|
36
|
+
* - the current conversation carries `aria-current="true"`, which is what makes "current" survive
|
|
37
|
+
* a theme that does not tint (WCAG 1.4.1 — never colour alone).
|
|
38
|
+
*/
|
|
39
|
+
export declare const Conversations: React.ForwardRefExoticComponent<ConversationsProp & React.RefAttributes<HTMLDivElement>>;
|
|
@@ -0,0 +1,371 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
3
|
+
import * as React from "react";
|
|
4
|
+
import { ChevronDown, MoreHorizontal, Plus } from "lucide-react";
|
|
5
|
+
import { useTranslation } from "../../i18n/use-translation.js";
|
|
6
|
+
import { formatDate } from "../../lib/datetime/format-date.js";
|
|
7
|
+
import { cn } from "../../lib/utils.js";
|
|
8
|
+
import { Button } from "../general/button.js";
|
|
9
|
+
import { Text } from "../general/typography.js";
|
|
10
|
+
import { Separator } from "../layout/separator.js";
|
|
11
|
+
import {
|
|
12
|
+
DropdownMenu,
|
|
13
|
+
DropdownMenuContent,
|
|
14
|
+
DropdownMenuItem,
|
|
15
|
+
DropdownMenuTrigger
|
|
16
|
+
} from "./dropdown-menu.js";
|
|
17
|
+
function isDivider(entry) {
|
|
18
|
+
return "type" in entry && entry.type === "divider";
|
|
19
|
+
}
|
|
20
|
+
function isRtl(element) {
|
|
21
|
+
return element?.closest("[dir]")?.getAttribute("dir")?.toLowerCase() === "rtl";
|
|
22
|
+
}
|
|
23
|
+
function toSections(items, groupable) {
|
|
24
|
+
const collapsibleOption = typeof groupable === "object" ? groupable.collapsible : void 0;
|
|
25
|
+
const sections = [];
|
|
26
|
+
for (const entry of items) {
|
|
27
|
+
const group = !groupable || isDivider(entry) ? void 0 : entry.group;
|
|
28
|
+
if (group === void 0) {
|
|
29
|
+
const tail = sections[sections.length - 1];
|
|
30
|
+
if (tail && tail.group === void 0) {
|
|
31
|
+
tail.entries.push(entry);
|
|
32
|
+
continue;
|
|
33
|
+
}
|
|
34
|
+
sections.push({ entries: [entry], collapsible: false });
|
|
35
|
+
continue;
|
|
36
|
+
}
|
|
37
|
+
const existing = sections.find((section) => section.group === group);
|
|
38
|
+
if (existing) {
|
|
39
|
+
existing.entries.push(entry);
|
|
40
|
+
continue;
|
|
41
|
+
}
|
|
42
|
+
sections.push({
|
|
43
|
+
group,
|
|
44
|
+
entries: [entry],
|
|
45
|
+
collapsible: typeof collapsibleOption === "function" ? collapsibleOption(group) : collapsibleOption ?? false
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
return sections;
|
|
49
|
+
}
|
|
50
|
+
const groupNavKey = (group) => `group:${group}`;
|
|
51
|
+
const rowNavKey = (key) => `row:${key}`;
|
|
52
|
+
const Conversations = React.forwardRef(
|
|
53
|
+
({
|
|
54
|
+
items = [],
|
|
55
|
+
activeKey,
|
|
56
|
+
defaultActiveKey,
|
|
57
|
+
onActiveChange,
|
|
58
|
+
menu,
|
|
59
|
+
groupable,
|
|
60
|
+
creation,
|
|
61
|
+
label,
|
|
62
|
+
id,
|
|
63
|
+
className,
|
|
64
|
+
...rest
|
|
65
|
+
}, ref) => {
|
|
66
|
+
const { t } = useTranslation();
|
|
67
|
+
const rootRef = React.useRef(null);
|
|
68
|
+
const setRootRef = React.useCallback(
|
|
69
|
+
(node) => {
|
|
70
|
+
rootRef.current = node;
|
|
71
|
+
if (typeof ref === "function") ref(node);
|
|
72
|
+
else if (ref) ref.current = node;
|
|
73
|
+
},
|
|
74
|
+
[ref]
|
|
75
|
+
);
|
|
76
|
+
const [uncontrolledActive, setUncontrolledActive] = React.useState(defaultActiveKey);
|
|
77
|
+
const mergedActiveKey = activeKey ?? uncontrolledActive;
|
|
78
|
+
const sections = React.useMemo(() => toSections(items, groupable), [items, groupable]);
|
|
79
|
+
const groupOptions = typeof groupable === "object" ? groupable : {};
|
|
80
|
+
const collapsibleGroups = React.useMemo(
|
|
81
|
+
() => sections.filter((section) => section.collapsible && section.group).map((s) => s.group),
|
|
82
|
+
[sections]
|
|
83
|
+
);
|
|
84
|
+
const [uncontrolledExpanded, setUncontrolledExpanded] = React.useState(() => [
|
|
85
|
+
...groupOptions.defaultExpandedKeys ?? collapsibleGroups
|
|
86
|
+
]);
|
|
87
|
+
const expandedKeys = groupOptions.expandedKeys ? [...groupOptions.expandedKeys] : uncontrolledExpanded;
|
|
88
|
+
const toggleGroup = (group) => {
|
|
89
|
+
const next = expandedKeys.includes(group) ? expandedKeys.filter((key) => key !== group) : [...expandedKeys, group];
|
|
90
|
+
if (!groupOptions.expandedKeys) setUncontrolledExpanded(next);
|
|
91
|
+
groupOptions.onExpand?.(next);
|
|
92
|
+
};
|
|
93
|
+
const navKeys = [];
|
|
94
|
+
for (const section of sections) {
|
|
95
|
+
if (section.group && section.collapsible) {
|
|
96
|
+
navKeys.push(groupNavKey(section.group));
|
|
97
|
+
if (!expandedKeys.includes(section.group)) continue;
|
|
98
|
+
}
|
|
99
|
+
for (const entry of section.entries) {
|
|
100
|
+
if (!isDivider(entry) && !entry.disabled) navKeys.push(rowNavKey(entry.key));
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
const [focusKey, setFocusKey] = React.useState(null);
|
|
104
|
+
const activeNavKey = mergedActiveKey ? rowNavKey(mergedActiveKey) : void 0;
|
|
105
|
+
const tabStop = focusKey && navKeys.includes(focusKey) ? focusKey : activeNavKey && navKeys.includes(activeNavKey) ? activeNavKey : navKeys[0];
|
|
106
|
+
const nodes = React.useRef(/* @__PURE__ */ new Map());
|
|
107
|
+
const registerNode = (key) => (node) => {
|
|
108
|
+
if (node) nodes.current.set(key, node);
|
|
109
|
+
else nodes.current.delete(key);
|
|
110
|
+
};
|
|
111
|
+
const moveTo = (key) => {
|
|
112
|
+
if (!key) return;
|
|
113
|
+
setFocusKey(key);
|
|
114
|
+
nodes.current.get(key)?.focus();
|
|
115
|
+
};
|
|
116
|
+
const step = (from, delta) => {
|
|
117
|
+
if (navKeys.length === 0) return;
|
|
118
|
+
const index = navKeys.indexOf(from);
|
|
119
|
+
const next = index === -1 ? 0 : (index + delta + navKeys.length) % navKeys.length;
|
|
120
|
+
moveTo(navKeys[next]);
|
|
121
|
+
};
|
|
122
|
+
const handleNavKeyDown = (navKey) => (event) => {
|
|
123
|
+
const rtl = isRtl(event.currentTarget);
|
|
124
|
+
const forward = rtl ? "ArrowLeft" : "ArrowRight";
|
|
125
|
+
const backward = rtl ? "ArrowRight" : "ArrowLeft";
|
|
126
|
+
switch (event.key) {
|
|
127
|
+
case "ArrowDown":
|
|
128
|
+
event.preventDefault();
|
|
129
|
+
step(navKey, 1);
|
|
130
|
+
return;
|
|
131
|
+
case "ArrowUp":
|
|
132
|
+
event.preventDefault();
|
|
133
|
+
step(navKey, -1);
|
|
134
|
+
return;
|
|
135
|
+
case "Home":
|
|
136
|
+
event.preventDefault();
|
|
137
|
+
moveTo(navKeys[0]);
|
|
138
|
+
return;
|
|
139
|
+
case "End":
|
|
140
|
+
event.preventDefault();
|
|
141
|
+
moveTo(navKeys[navKeys.length - 1]);
|
|
142
|
+
return;
|
|
143
|
+
case forward: {
|
|
144
|
+
const trigger = nodes.current.get(`menu:${navKey}`);
|
|
145
|
+
if (!trigger) return;
|
|
146
|
+
event.preventDefault();
|
|
147
|
+
trigger.focus();
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
150
|
+
case backward: {
|
|
151
|
+
if (!navKey.startsWith("menu:")) return;
|
|
152
|
+
event.preventDefault();
|
|
153
|
+
moveTo(navKey.slice("menu:".length));
|
|
154
|
+
return;
|
|
155
|
+
}
|
|
156
|
+
default:
|
|
157
|
+
}
|
|
158
|
+
};
|
|
159
|
+
const selectConversation = (item) => {
|
|
160
|
+
if (item.disabled) return;
|
|
161
|
+
if (activeKey === void 0) setUncontrolledActive(item.key);
|
|
162
|
+
onActiveChange?.(
|
|
163
|
+
item.key,
|
|
164
|
+
items.find((entry) => !isDivider(entry) && entry.key === item.key)
|
|
165
|
+
);
|
|
166
|
+
};
|
|
167
|
+
const menuFor = (item) => {
|
|
168
|
+
const resolved = typeof menu === "function" ? menu(item) : menu;
|
|
169
|
+
return resolved && resolved.items.length > 0 ? resolved : void 0;
|
|
170
|
+
};
|
|
171
|
+
const renderEntry = (entry, index) => {
|
|
172
|
+
if (isDivider(entry)) {
|
|
173
|
+
return /* @__PURE__ */ jsx("li", { className: "ui-conversations-rule", children: /* @__PURE__ */ jsx(
|
|
174
|
+
Separator,
|
|
175
|
+
{
|
|
176
|
+
"data-dashed": entry.dashed ? "true" : void 0,
|
|
177
|
+
className: "ui-conversations-separator"
|
|
178
|
+
}
|
|
179
|
+
) }, entry.key ?? `divider-${index}`);
|
|
180
|
+
}
|
|
181
|
+
const navKey = rowNavKey(entry.key);
|
|
182
|
+
const active = mergedActiveKey === entry.key;
|
|
183
|
+
const rowMenu = entry.disabled ? void 0 : menuFor(entry);
|
|
184
|
+
const labelText = typeof entry.label === "string" ? entry.label : entry.key;
|
|
185
|
+
return /* @__PURE__ */ jsxs(
|
|
186
|
+
"li",
|
|
187
|
+
{
|
|
188
|
+
"data-slot": "conversations-item",
|
|
189
|
+
"data-active": active ? "true" : void 0,
|
|
190
|
+
className: "ui-conversations-item",
|
|
191
|
+
children: [
|
|
192
|
+
/* @__PURE__ */ jsxs(
|
|
193
|
+
Button,
|
|
194
|
+
{
|
|
195
|
+
ref: registerNode(navKey),
|
|
196
|
+
type: "button",
|
|
197
|
+
variant: "ghost",
|
|
198
|
+
fullWidth: true,
|
|
199
|
+
align: "start",
|
|
200
|
+
disabled: entry.disabled,
|
|
201
|
+
"aria-current": active ? "true" : void 0,
|
|
202
|
+
tabIndex: tabStop === navKey ? 0 : -1,
|
|
203
|
+
className: "ui-conversations-row",
|
|
204
|
+
onFocus: () => setFocusKey(navKey),
|
|
205
|
+
onClick: () => selectConversation(entry),
|
|
206
|
+
onKeyDown: handleNavKeyDown(navKey),
|
|
207
|
+
children: [
|
|
208
|
+
entry.icon ? /* @__PURE__ */ jsx("span", { className: "ui-conversations-icon", "aria-hidden": "true", children: entry.icon }) : null,
|
|
209
|
+
/* @__PURE__ */ jsxs("span", { className: "ui-conversations-line", children: [
|
|
210
|
+
/* @__PURE__ */ jsx(Text, { truncate: true, children: entry.label }),
|
|
211
|
+
entry.timestamp === void 0 ? null : (
|
|
212
|
+
/*
|
|
213
|
+
* Through `formatDate`, never through `toLocaleString` or a sliced ISO string.
|
|
214
|
+
* `docs/DATETIME.md` rule 1 forbids the alternatives for a measured reason: the
|
|
215
|
+
* tenant's locale, timezone and 12/24h choice live in AppProvider, so the same
|
|
216
|
+
* epoch has to read 2026/03/15 in a Japanese tenant and 15/03/2026 in a
|
|
217
|
+
* Vietnamese one. A component that formats its own dates cannot do that.
|
|
218
|
+
*/
|
|
219
|
+
/* @__PURE__ */ jsx(Text, { size: "xs", tone: "muted", tabular: true, className: "ui-conversations-timestamp", children: formatDate(new Date(entry.timestamp)) })
|
|
220
|
+
)
|
|
221
|
+
] })
|
|
222
|
+
]
|
|
223
|
+
}
|
|
224
|
+
),
|
|
225
|
+
rowMenu ? /* @__PURE__ */ jsxs(DropdownMenu, { children: [
|
|
226
|
+
/* @__PURE__ */ jsx(DropdownMenuTrigger, { asChild: true, children: /* @__PURE__ */ jsx(
|
|
227
|
+
Button,
|
|
228
|
+
{
|
|
229
|
+
ref: registerNode(`menu:${navKey}`),
|
|
230
|
+
type: "button",
|
|
231
|
+
variant: "ghost",
|
|
232
|
+
size: "icon-sm",
|
|
233
|
+
tabIndex: -1,
|
|
234
|
+
className: "ui-conversations-menu-trigger",
|
|
235
|
+
"aria-label": rowMenu.triggerLabel?.(entry) ?? t("navigation.conversations.rowActions", { label: labelText }),
|
|
236
|
+
onKeyDown: handleNavKeyDown(`menu:${navKey}`),
|
|
237
|
+
children: /* @__PURE__ */ jsx(MoreHorizontal, { "aria-hidden": "true" })
|
|
238
|
+
}
|
|
239
|
+
) }),
|
|
240
|
+
/* @__PURE__ */ jsx(DropdownMenuContent, { align: "end", children: rowMenu.items.map((command) => /* @__PURE__ */ jsxs(
|
|
241
|
+
DropdownMenuItem,
|
|
242
|
+
{
|
|
243
|
+
disabled: command.disabled,
|
|
244
|
+
variant: command.danger ? "destructive" : void 0,
|
|
245
|
+
onSelect: () => rowMenu.onClick?.({ key: command.key, conversation: entry }),
|
|
246
|
+
children: [
|
|
247
|
+
command.icon,
|
|
248
|
+
command.label
|
|
249
|
+
]
|
|
250
|
+
},
|
|
251
|
+
command.key
|
|
252
|
+
)) })
|
|
253
|
+
] }) : null
|
|
254
|
+
]
|
|
255
|
+
},
|
|
256
|
+
entry.key
|
|
257
|
+
);
|
|
258
|
+
};
|
|
259
|
+
const railLabel = label ?? t("navigation.conversations.label");
|
|
260
|
+
return /* @__PURE__ */ jsxs(
|
|
261
|
+
"div",
|
|
262
|
+
{
|
|
263
|
+
ref: setRootRef,
|
|
264
|
+
id,
|
|
265
|
+
"data-slot": "conversations",
|
|
266
|
+
className: cn("ui-conversations", className),
|
|
267
|
+
...rest,
|
|
268
|
+
children: [
|
|
269
|
+
creation ? /* @__PURE__ */ jsx(CreationButton, { creation }) : null,
|
|
270
|
+
sections.map((section, sectionIndex) => {
|
|
271
|
+
if (!section.group) {
|
|
272
|
+
return /* @__PURE__ */ jsx(
|
|
273
|
+
"ul",
|
|
274
|
+
{
|
|
275
|
+
"aria-label": railLabel,
|
|
276
|
+
className: "ui-conversations-list",
|
|
277
|
+
children: section.entries.map(renderEntry)
|
|
278
|
+
},
|
|
279
|
+
`run-${sectionIndex}`
|
|
280
|
+
);
|
|
281
|
+
}
|
|
282
|
+
const heading = typeof groupOptions.label === "function" ? groupOptions.label(section.group) : groupOptions.label ?? section.group;
|
|
283
|
+
const navKey = groupNavKey(section.group);
|
|
284
|
+
const expanded = expandedKeys.includes(section.group);
|
|
285
|
+
const listId = `${id ?? "ui-conversations"}-${section.group}`;
|
|
286
|
+
return /* @__PURE__ */ jsxs(
|
|
287
|
+
"div",
|
|
288
|
+
{
|
|
289
|
+
"data-slot": "conversations-group",
|
|
290
|
+
className: "ui-conversations-group",
|
|
291
|
+
children: [
|
|
292
|
+
section.collapsible ? /* @__PURE__ */ jsxs(
|
|
293
|
+
Button,
|
|
294
|
+
{
|
|
295
|
+
ref: registerNode(navKey),
|
|
296
|
+
type: "button",
|
|
297
|
+
variant: "ghost",
|
|
298
|
+
size: "sm",
|
|
299
|
+
fullWidth: true,
|
|
300
|
+
align: "start",
|
|
301
|
+
"aria-expanded": expanded,
|
|
302
|
+
"aria-controls": listId,
|
|
303
|
+
tabIndex: tabStop === navKey ? 0 : -1,
|
|
304
|
+
className: "ui-conversations-group-trigger",
|
|
305
|
+
onFocus: () => setFocusKey(navKey),
|
|
306
|
+
onClick: () => toggleGroup(section.group),
|
|
307
|
+
onKeyDown: handleNavKeyDown(navKey),
|
|
308
|
+
children: [
|
|
309
|
+
/* @__PURE__ */ jsx(
|
|
310
|
+
ChevronDown,
|
|
311
|
+
{
|
|
312
|
+
"aria-hidden": "true",
|
|
313
|
+
className: "ui-conversations-group-chevron",
|
|
314
|
+
"data-expanded": expanded ? "true" : void 0
|
|
315
|
+
}
|
|
316
|
+
),
|
|
317
|
+
/* @__PURE__ */ jsx(Text, { size: "xs", tone: "muted", weight: "medium", truncate: true, children: heading })
|
|
318
|
+
]
|
|
319
|
+
}
|
|
320
|
+
) : /* @__PURE__ */ jsx(
|
|
321
|
+
Text,
|
|
322
|
+
{
|
|
323
|
+
size: "xs",
|
|
324
|
+
tone: "muted",
|
|
325
|
+
weight: "medium",
|
|
326
|
+
className: "ui-conversations-group-label",
|
|
327
|
+
children: heading
|
|
328
|
+
}
|
|
329
|
+
),
|
|
330
|
+
section.collapsible && !expanded ? null : /* @__PURE__ */ jsx(
|
|
331
|
+
"ul",
|
|
332
|
+
{
|
|
333
|
+
id: listId,
|
|
334
|
+
"aria-label": typeof heading === "string" ? heading : section.group,
|
|
335
|
+
className: "ui-conversations-list",
|
|
336
|
+
children: section.entries.map(renderEntry)
|
|
337
|
+
}
|
|
338
|
+
)
|
|
339
|
+
]
|
|
340
|
+
},
|
|
341
|
+
section.group
|
|
342
|
+
);
|
|
343
|
+
})
|
|
344
|
+
]
|
|
345
|
+
}
|
|
346
|
+
);
|
|
347
|
+
}
|
|
348
|
+
);
|
|
349
|
+
Conversations.displayName = "Conversations";
|
|
350
|
+
function CreationButton({ creation }) {
|
|
351
|
+
const { t } = useTranslation();
|
|
352
|
+
return /* @__PURE__ */ jsxs(
|
|
353
|
+
Button,
|
|
354
|
+
{
|
|
355
|
+
type: "button",
|
|
356
|
+
variant: "outline",
|
|
357
|
+
fullWidth: true,
|
|
358
|
+
align: "start",
|
|
359
|
+
disabled: creation.disabled,
|
|
360
|
+
className: "ui-conversations-creation",
|
|
361
|
+
onClick: () => creation.onClick?.(),
|
|
362
|
+
children: [
|
|
363
|
+
creation.icon ?? /* @__PURE__ */ jsx(Plus, { "aria-hidden": "true" }),
|
|
364
|
+
creation.label ?? t("navigation.conversations.creation")
|
|
365
|
+
]
|
|
366
|
+
}
|
|
367
|
+
);
|
|
368
|
+
}
|
|
369
|
+
export {
|
|
370
|
+
Conversations
|
|
371
|
+
};
|
|
@@ -9,3 +9,5 @@ export { AppSettingToggle } from "./app-setting-toggle.js";
|
|
|
9
9
|
export type { AppSettingToggleProp, AppSettingToggleProps, AppSettingToggleKind, } from "./app-setting-toggle.js";
|
|
10
10
|
export type { BreadcrumbItemProp as BreadcrumbItem } from "../../props/vocabulary/navigation.prop.js";
|
|
11
11
|
export { DropdownMenu, DropdownMenuCheckboxItem, DropdownMenuContent, DropdownMenuGroup, DropdownMenuItem, DropdownMenuLabel, DropdownMenuPortal, DropdownMenuRadioGroup, DropdownMenuRadioItem, DropdownMenuSeparator, DropdownMenuShortcut, DropdownMenuSub, DropdownMenuSubContent, DropdownMenuSubTrigger, DropdownMenuTrigger, } from "./dropdown-menu.js";
|
|
12
|
+
export { Conversations } from "./conversations.js";
|
|
13
|
+
export type { ConversationsProp, ConversationsProps, ConversationsItemProp, ConversationsDividerProp, ConversationsEntryProp, ConversationsMenuProp, ConversationsMenuItemProp, ConversationsGroupableProp, ConversationsCreationProp, } from "./conversations.js";
|