@godxjp/ui 31.28.3 → 31.29.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.
Files changed (61) hide show
  1. package/agent/START-HERE.md +5 -5
  2. package/agent/components/AppLauncher.json +5 -0
  3. package/agent/components/Avatar.json +1 -0
  4. package/agent/components/Dialog.json +3 -1
  5. package/agent/components/ResponsiveGrid.json +7 -0
  6. package/agent/components/Sidebar.json +5 -0
  7. package/agent/components/SortableList.json +78 -0
  8. package/agent/components/TopbarItem.json +1 -1
  9. package/agent/components-index.json +5 -0
  10. package/agent/components.json +100 -2
  11. package/agent/index.json +6 -6
  12. package/agent/llms.txt +6 -6
  13. package/agent/tokens.json +74 -2
  14. package/dist/components/data-entry/index.d.ts +2 -0
  15. package/dist/components/data-entry/index.js +2 -0
  16. package/dist/components/data-entry/sortable-list.d.ts +10 -0
  17. package/dist/components/data-entry/sortable-list.js +237 -0
  18. package/dist/components/feedback/dialog.d.ts +8 -2
  19. package/dist/components/feedback/dialog.js +8 -1
  20. package/dist/components/feedback/sheet.js +1 -1
  21. package/dist/components/layout/app-launcher.d.ts +1 -1
  22. package/dist/components/layout/app-launcher.js +18 -14
  23. package/dist/components/layout/responsive-grid.d.ts +17 -2
  24. package/dist/components/layout/responsive-grid.js +17 -4
  25. package/dist/components/layout/sidebar.d.ts +1 -1
  26. package/dist/components/layout/sidebar.js +3 -0
  27. package/dist/components/layout/topbar-item.js +8 -2
  28. package/dist/components/ui/avatar.d.ts +3 -1
  29. package/dist/components/ui/avatar.js +2 -1
  30. package/dist/contracts/measurement.json +1 -1
  31. package/dist/i18n/messages/en.json +8 -0
  32. package/dist/i18n/messages/ja.json +8 -0
  33. package/dist/i18n/messages/vi.json +8 -0
  34. package/dist/props/components/data-display.prop.d.ts +9 -0
  35. package/dist/props/components/data-entry.prop.d.ts +34 -0
  36. package/dist/props/components/feedback.prop.d.ts +6 -1
  37. package/dist/props/components/layout.prop.d.ts +22 -0
  38. package/dist/props/registry.d.ts +33 -2
  39. package/dist/props/registry.js +44 -1
  40. package/dist/styles/control.css +50 -0
  41. package/dist/styles/data-display-layout.css +4 -0
  42. package/dist/styles/dialog-layout.css +14 -1
  43. package/dist/styles/layers.json +6 -1
  44. package/dist/styles/layout.css +29 -7
  45. package/dist/styles/shell-layout.css +26 -0
  46. package/dist/tokens/components/control.css +5 -0
  47. package/dist/tokens/components/feedback.css +6 -0
  48. package/dist/tokens/components/shell.css +6 -0
  49. package/dist/tokens/semantic/layout.css +1 -4
  50. package/docs/FRAME-COVERAGE-REPORT.md +3 -2
  51. package/docs/data-display/avatar.tsx +32 -0
  52. package/docs/data-entry/sortable-list.tsx +76 -0
  53. package/docs/feedback/dialog.tsx +37 -1
  54. package/docs/i18n/messages/en.json +51 -0
  55. package/docs/i18n/messages/ja.json +51 -0
  56. package/docs/i18n/messages/vi.json +51 -0
  57. package/docs/layout/app-launcher.tsx +17 -1
  58. package/docs/layout/responsive-grid.tsx +23 -0
  59. package/docs/layout/sidebar.tsx +24 -0
  60. package/docs/layout/topbar-item.tsx +11 -0
  61. package/package.json +2 -2
@@ -0,0 +1,237 @@
1
+ "use client";
2
+ import { jsx, jsxs } from "react/jsx-runtime";
3
+ import * as React from "react";
4
+ import { GripVertical } from "lucide-react";
5
+ import { useTranslation } from "../../i18n/use-translation.js";
6
+ import { cn } from "../../lib/utils.js";
7
+ import { Button } from "../general/button.js";
8
+ const DRAG_THRESHOLD = 4;
9
+ function move(list, from, to) {
10
+ const next = list.slice();
11
+ const [item] = next.splice(from, 1);
12
+ next.splice(to, 0, item);
13
+ return next;
14
+ }
15
+ function reconcile(order, items) {
16
+ const known = new Set(items.map((item) => item.value));
17
+ const kept = order.filter((key) => known.has(key));
18
+ const seen = new Set(kept);
19
+ return [...kept, ...items.map((item) => item.value).filter((key) => !seen.has(key))];
20
+ }
21
+ const SortableList = React.forwardRef(
22
+ function SortableList2({
23
+ items,
24
+ value: valueProp,
25
+ defaultValue,
26
+ onValueChange,
27
+ renderItem,
28
+ layout = "list",
29
+ disabled = false,
30
+ "aria-label": ariaLabel,
31
+ id,
32
+ className
33
+ }, ref) {
34
+ const { t, locale } = useTranslation();
35
+ const hintId = React.useId();
36
+ const [inner, setInner] = React.useState(
37
+ () => defaultValue ?? items.map((item) => item.value)
38
+ );
39
+ const committed = reconcile(valueProp ?? inner, items);
40
+ const [preview, setPreview] = React.useState(null);
41
+ const [held, setHeld] = React.useState(
42
+ null
43
+ );
44
+ const [announcement, setAnnouncement] = React.useState("");
45
+ const order = preview ?? committed;
46
+ const byKey = React.useMemo(() => new Map(items.map((item) => [item.value, item])), [items]);
47
+ const listRef = React.useRef(null);
48
+ const number = React.useMemo(() => new Intl.NumberFormat(locale), [locale]);
49
+ const position = (keys, key) => ({
50
+ label: byKey.get(key)?.label ?? key,
51
+ position: number.format(keys.indexOf(key) + 1),
52
+ count: number.format(keys.length)
53
+ });
54
+ const commit = (keys) => {
55
+ if (keys.join("\0") !== committed.join("\0")) {
56
+ if (valueProp === void 0) setInner(keys);
57
+ onValueChange?.(keys);
58
+ }
59
+ };
60
+ const lift = (key, via) => {
61
+ setHeld({ key, via });
62
+ setPreview(committed);
63
+ setAnnouncement(t("dataEntry.sortableList.lifted", position(committed, key)));
64
+ };
65
+ const drop = () => {
66
+ if (!held || !preview) return;
67
+ commit(preview);
68
+ setAnnouncement(t("dataEntry.sortableList.dropped", position(preview, held.key)));
69
+ setHeld(null);
70
+ setPreview(null);
71
+ };
72
+ const cancel = () => {
73
+ if (!held) return;
74
+ setAnnouncement(t("dataEntry.sortableList.cancelled", position(committed, held.key)));
75
+ setHeld(null);
76
+ setPreview(null);
77
+ };
78
+ const moveTo = (key, target) => {
79
+ setPreview((current) => {
80
+ const keys = current ?? committed;
81
+ const from = keys.indexOf(key);
82
+ const to = Math.max(0, Math.min(keys.length - 1, target));
83
+ if (from < 0 || from === to) return keys;
84
+ const next = move(keys, from, to);
85
+ setAnnouncement(t("dataEntry.sortableList.moved", position(next, key)));
86
+ return next;
87
+ });
88
+ };
89
+ const onKeyDown = (key) => (event) => {
90
+ if (event.key === " " || event.key === "Enter") {
91
+ event.preventDefault();
92
+ if (held?.key === key) drop();
93
+ else if (!held) lift(key, "keyboard");
94
+ return;
95
+ }
96
+ if (held?.key !== key) return;
97
+ const index = order.indexOf(key);
98
+ const step = {
99
+ ArrowUp: -1,
100
+ ArrowLeft: -1,
101
+ ArrowDown: 1,
102
+ ArrowRight: 1
103
+ };
104
+ if (event.key in step) {
105
+ event.preventDefault();
106
+ moveTo(key, index + (step[event.key] ?? 0));
107
+ } else if (event.key === "Home") {
108
+ event.preventDefault();
109
+ moveTo(key, 0);
110
+ } else if (event.key === "End") {
111
+ event.preventDefault();
112
+ moveTo(key, order.length - 1);
113
+ } else if (event.key === "Escape") {
114
+ event.preventDefault();
115
+ cancel();
116
+ }
117
+ };
118
+ const indexAt = (x, y) => {
119
+ const nodes = Array.from(
120
+ listRef.current?.querySelectorAll("[data-sortable-value]") ?? []
121
+ );
122
+ let best = -1;
123
+ let bestDistance = Number.POSITIVE_INFINITY;
124
+ nodes.forEach((node, index) => {
125
+ const box = node.getBoundingClientRect();
126
+ if (x >= box.left && x <= box.right && y >= box.top && y <= box.bottom) {
127
+ best = index;
128
+ bestDistance = -1;
129
+ return;
130
+ }
131
+ if (bestDistance < 0) return;
132
+ const distance = Math.hypot(x - (box.left + box.width / 2), y - (box.top + box.height / 2));
133
+ if (distance < bestDistance) {
134
+ bestDistance = distance;
135
+ best = index;
136
+ }
137
+ });
138
+ return best;
139
+ };
140
+ const press = React.useRef(
141
+ null
142
+ );
143
+ const latest = React.useRef({ lift, moveTo, drop, cancel, indexAt });
144
+ latest.current = { lift, moveTo, drop, cancel, indexAt };
145
+ const onPointerDown = (key) => (event) => {
146
+ if (event.button !== 0 || held) return;
147
+ press.current = { key, x: event.clientX, y: event.clientY, dragging: false };
148
+ const onMove = (move2) => {
149
+ const current = press.current;
150
+ if (!current) return;
151
+ if (!current.dragging) {
152
+ if (Math.hypot(move2.clientX - current.x, move2.clientY - current.y) < DRAG_THRESHOLD)
153
+ return;
154
+ current.dragging = true;
155
+ latest.current.lift(current.key, "pointer");
156
+ }
157
+ const target = latest.current.indexAt(move2.clientX, move2.clientY);
158
+ if (target >= 0) latest.current.moveTo(current.key, target);
159
+ };
160
+ const end = (cancelled) => () => {
161
+ window.removeEventListener("pointermove", onMove);
162
+ window.removeEventListener("pointerup", onUp);
163
+ window.removeEventListener("pointercancel", onCancel);
164
+ window.removeEventListener("keydown", onKey);
165
+ const current = press.current;
166
+ press.current = null;
167
+ if (!current?.dragging) return;
168
+ if (cancelled) latest.current.cancel();
169
+ else latest.current.drop();
170
+ };
171
+ const onUp = end(false);
172
+ const onCancel = end(true);
173
+ const onKey = (key2) => {
174
+ if (key2.key === "Escape" && press.current?.dragging) end(true)();
175
+ };
176
+ window.addEventListener("pointermove", onMove);
177
+ window.addEventListener("pointerup", onUp);
178
+ window.addEventListener("pointercancel", onCancel);
179
+ window.addEventListener("keydown", onKey);
180
+ };
181
+ return /* @__PURE__ */ jsxs(
182
+ "div",
183
+ {
184
+ ref,
185
+ id,
186
+ className: cn("ui-sortable-list", className),
187
+ "data-layout": layout,
188
+ "data-disabled": disabled || void 0,
189
+ children: [
190
+ /* @__PURE__ */ jsx("ul", { ref: listRef, className: "ui-sortable-list-items", "aria-label": ariaLabel, children: order.map((key, index) => {
191
+ const item = byKey.get(key);
192
+ if (!item) return null;
193
+ const dragging = held?.key === key;
194
+ const itemDisabled = disabled || item.disabled;
195
+ return /* @__PURE__ */ jsxs(
196
+ "li",
197
+ {
198
+ className: "ui-sortable-list-item",
199
+ "data-sortable-value": key,
200
+ "data-dragging": dragging || void 0,
201
+ "data-disabled": itemDisabled || void 0,
202
+ children: [
203
+ /* @__PURE__ */ jsx(
204
+ Button,
205
+ {
206
+ type: "button",
207
+ variant: "ghost",
208
+ size: "icon-sm",
209
+ className: "ui-sortable-list-handle",
210
+ disabled: itemDisabled,
211
+ "aria-label": t("dataEntry.sortableList.handle", { label: item.label }),
212
+ "aria-describedby": hintId,
213
+ "aria-pressed": dragging,
214
+ onKeyDown: onKeyDown(key),
215
+ onBlur: () => {
216
+ if (held?.key === key && held.via === "keyboard") cancel();
217
+ },
218
+ onPointerDown: onPointerDown(key),
219
+ children: /* @__PURE__ */ jsx(GripVertical, { "aria-hidden": "true" })
220
+ }
221
+ ),
222
+ /* @__PURE__ */ jsx("div", { className: "ui-sortable-list-content", children: renderItem ? renderItem(item, { index, dragging }) : item.label })
223
+ ]
224
+ },
225
+ key
226
+ );
227
+ }) }),
228
+ /* @__PURE__ */ jsx("span", { id: hintId, className: "sr-only", children: t("dataEntry.sortableList.hint") }),
229
+ /* @__PURE__ */ jsx("span", { role: "status", "aria-live": "polite", className: "sr-only", children: announcement })
230
+ ]
231
+ }
232
+ );
233
+ }
234
+ );
235
+ export {
236
+ SortableList
237
+ };
@@ -1,5 +1,5 @@
1
1
  import * as React from "react";
2
- import type { ToneProp } from "../../props/vocabulary/index.js";
2
+ import type { ToneProp, WidthProp } from "../../props/vocabulary/index.js";
3
3
  import type { AlertDialogProp, DialogContentProp, DialogProp } from "../../props/components/feedback.prop.js";
4
4
  export type { AlertDialogProp, AlertDialogProp as AlertDialogProps, DialogProp, DialogProp as DialogProps, DialogContentProp, DialogContentProp as DialogContentProps, } from "../../props/components/feedback.prop.js";
5
5
  interface OverlayOpenProps {
@@ -57,8 +57,14 @@ interface DialogContentProps extends Omit<React.ComponentPropsWithRef<"section">
57
57
  forceMount?: true;
58
58
  /** Giữ tên prop của Radix — xem `useCloseAutoFocus`. */
59
59
  onCloseAutoFocus?: (event: Event) => void;
60
+ /**
61
+ * Panel width (antd Modal `width`, the same vocabulary as SheetContent `width`). A number is px;
62
+ * a string is any CSS length. Capped at the viewport minus `--dialog-viewport-inset`, so a wide
63
+ * dialog never leaves a phone screen. Omit to keep `--dialog-width-default` (32rem).
64
+ */
65
+ width?: WidthProp;
60
66
  }
61
- declare function DialogContent({ className, children, showClose, showCloseButton: showCloseButtonProp, overlayClassName, variant: variantProp, forceMount: _forceMount, ...props }: DialogContentProps): React.JSX.Element;
67
+ declare function DialogContent({ className, children, showClose, showCloseButton: showCloseButtonProp, overlayClassName, variant: variantProp, forceMount: _forceMount, width, style, ...props }: DialogContentProps): React.JSX.Element;
62
68
  declare namespace DialogContent {
63
69
  var displayName: string;
64
70
  }
@@ -45,7 +45,7 @@ const DialogLabelContext = React.createContext(null);
45
45
  const DialogVariantContext = React.createContext("default");
46
46
  const DialogModalContext = React.createContext(true);
47
47
  const OVERLAY_CLASS = "ui-dialog-overlay data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:animate-in data-[state=open]:fade-in-0";
48
- const CONTENT_CLASS = "data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[state=open]:animate-in data-[state=open]:fade-in-0 data-[state=open]:zoom-in-95 duration-200 outline-none";
48
+ const CONTENT_CLASS = "data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[state=open]:animate-in data-[state=open]:fade-in-0 data-[state=open]:zoom-in-95 outline-none";
49
49
  function DialogShell({
50
50
  role = "dialog",
51
51
  className,
@@ -177,6 +177,8 @@ function DialogContent({
177
177
  overlayClassName,
178
178
  variant: variantProp,
179
179
  forceMount: _forceMount,
180
+ width,
181
+ style,
180
182
  ...props
181
183
  }) {
182
184
  const { t } = useTranslation();
@@ -184,6 +186,10 @@ function DialogContent({
184
186
  const variant = variantProp ?? inheritedVariant;
185
187
  const isDestructive = variant === "destructive";
186
188
  const showCloseButton = showCloseButtonProp ?? showClose ?? !isDestructive;
189
+ const mergedStyle = width == null ? style : {
190
+ ...style,
191
+ ["--dialog-width"]: typeof width === "number" ? `${width}px` : width
192
+ };
187
193
  return /* @__PURE__ */ jsx(DialogVariantContext.Provider, { value: variant, children: /* @__PURE__ */ jsxs(
188
194
  DialogShell,
189
195
  {
@@ -192,6 +198,7 @@ function DialogContent({
192
198
  "data-variant": variant,
193
199
  className,
194
200
  overlayClassName,
201
+ style: mergedStyle,
195
202
  ...props,
196
203
  children: [
197
204
  children,
@@ -91,7 +91,7 @@ function SheetOverlay(_props) {
91
91
  SheetOverlay.displayName = "SheetOverlay";
92
92
  const SHEET_OVERLAY_CLASS = "ui-sheet-overlay data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:animate-in data-[state=open]:fade-in-0 fixed";
93
93
  const sheetVariants = cva(
94
- "ui-sheet-panel fixed flex flex-col gap-[var(--space-chrome-gap)] text-foreground px-[var(--sheet-pad-x)] py-[var(--sheet-pad-y)] transition ease-in-out data-[state=closed]:animate-out data-[state=closed]:duration-300 data-[state=open]:animate-in data-[state=open]:duration-500",
94
+ "ui-sheet-panel fixed flex flex-col gap-[var(--space-chrome-gap)] text-foreground px-[var(--sheet-pad-x)] py-[var(--sheet-pad-y)] transition ease-in-out data-[state=closed]:animate-out data-[state=open]:animate-in",
95
95
  {
96
96
  variants: {
97
97
  // `side` is a deliberately PHYSICAL API (left/right/top/bottom) — a sheet
@@ -31,4 +31,4 @@ type DataAttributes = {
31
31
  * single product where switching workspace is constant enough to be worth permanent screen width
32
32
  * (Slack). Shipping both puts one scope in two places and makes neither authoritative.
33
33
  */
34
- export declare function AppLauncher({ apps, groups, labels, columns, linkComponent, loading, error, onRetry, responsive, appearance, side, align, open, onOpenChange, className, ...rest }: AppLauncherProp & Pick<React.ComponentPropsWithoutRef<"button">, "id"> & DataAttributes): React.JSX.Element;
34
+ export declare function AppLauncher({ apps, groups, labels, columns, linkComponent, loading, error, onRetry, responsive, appearance, side, align, footer, open, onOpenChange, className, ...rest }: AppLauncherProp & Pick<React.ComponentPropsWithoutRef<"button">, "id"> & DataAttributes): React.JSX.Element;
@@ -165,6 +165,7 @@ function AppLauncher({
165
165
  appearance = "bar",
166
166
  side,
167
167
  align,
168
+ footer,
168
169
  open,
169
170
  onOpenChange,
170
171
  className,
@@ -194,20 +195,23 @@ function AppLauncher({
194
195
  ...rest
195
196
  };
196
197
  const trigger = appearance === "bar" ? /* @__PURE__ */ jsx(TopbarItem, { ...triggerProps, children: /* @__PURE__ */ jsx(Grip, { "aria-hidden": "true" }) }) : /* @__PURE__ */ jsx(Button, { variant: "ghost", size: "icon-sm", ...triggerProps, children: /* @__PURE__ */ jsx(Grip, { "aria-hidden": "true" }) });
197
- const panel = /* @__PURE__ */ jsx(
198
- AppLauncherPanel,
199
- {
200
- apps,
201
- groups,
202
- labels,
203
- columns,
204
- linkComponent,
205
- loading,
206
- error,
207
- onRetry,
208
- close
209
- }
210
- );
198
+ const panel = /* @__PURE__ */ jsxs(Fragment, { children: [
199
+ /* @__PURE__ */ jsx(
200
+ AppLauncherPanel,
201
+ {
202
+ apps,
203
+ groups,
204
+ labels,
205
+ columns,
206
+ linkComponent,
207
+ loading,
208
+ error,
209
+ onRetry,
210
+ close
211
+ }
212
+ ),
213
+ footer != null && footer !== false ? /* @__PURE__ */ jsx("div", { "data-slot": "app-launcher-footer", className: "ui-app-launcher-footer", children: footer }) : null
214
+ ] });
211
215
  if (launchpad) {
212
216
  return /* @__PURE__ */ jsxs(Dialog, { open: resolvedOpen, onOpenChange: setOpen, children: [
213
217
  /* @__PURE__ */ jsx(DialogTrigger, { asChild: true, children: trigger }),
@@ -24,15 +24,30 @@ export type ResponsiveGridProps = {
24
24
  * to strip its old `columns` prop.
25
25
  */
26
26
  preset?: ResponsiveGridPresetProp;
27
+ /**
28
+ * Dense packing (`grid-auto-flow: row dense`): a later, smaller cell back-fills the hole a wide
29
+ * or tall cell left earlier in the row — a widget board of 1×1 / 2×1 / 1×2 cells without gaps
30
+ * (gh#1175). It changes VISUAL order only; DOM and focus order stay as written, so use it where
31
+ * the cells are independent (widgets, tiles), not where reading order matters. Ignored under
32
+ * `flow="columns"`, whose sequence is the point.
33
+ */
34
+ dense?: boolean;
27
35
  children: ReactNode;
28
36
  };
29
- export declare function ResponsiveGrid({ columns, flow, align, gap, pad, padRaw, preset, className, children, }: ResponsiveGridProps): import("react").JSX.Element;
37
+ export declare function ResponsiveGrid({ columns, flow, align, gap, pad, padRaw, preset, dense, className, children, }: ResponsiveGridProps): import("react").JSX.Element;
30
38
  export declare namespace ResponsiveGrid {
31
39
  export { ResponsiveGridItem as Item };
32
40
  }
33
41
  /** A grid item owns its responsive span; no extra implicit columns on a narrow container. */
34
- export declare function ResponsiveGridItem({ span, children, className, }: {
42
+ export declare function ResponsiveGridItem({ span, rowSpan, children, className, }: {
35
43
  span?: ResponsiveGridColumnsProp;
44
+ /**
45
+ * Rows the cell spans — the tall (1×2) widget of a board (gh#1175). Same shape as `span`: a
46
+ * number spans that many rows from the `sm` step up and one row at `base`, where the grid is a
47
+ * single stacked column and a tall cell would only stretch; an object sets each step. Pair with
48
+ * `ResponsiveGrid dense` so the cells after it fill the space beside it.
49
+ */
50
+ rowSpan?: ResponsiveGridColumnsProp;
36
51
  children?: ReactNode;
37
52
  className?: string;
38
53
  }): import("react").JSX.Element;
@@ -36,6 +36,7 @@ function ResponsiveGrid({
36
36
  pad,
37
37
  padRaw,
38
38
  preset,
39
+ dense = false,
39
40
  className,
40
41
  children
41
42
  }) {
@@ -47,21 +48,33 @@ function ResponsiveGrid({
47
48
  "data-gap": gap,
48
49
  "data-flow": flow === "columns" ? "columns" : void 0,
49
50
  "data-align": align,
51
+ "data-dense": dense && flow !== "columns" ? "" : void 0,
50
52
  style: { ...toStyle(resolved), ...padStyle(pad, padRaw) },
51
53
  "data-pad-raw": padRaw === void 0 ? void 0 : "",
52
54
  children
53
55
  }
54
56
  ) });
55
57
  }
58
+ function resolveSpan(span) {
59
+ return typeof span === "number" ? { base: 1, sm: span, md: span, lg: span } : resolveColumns(span);
60
+ }
56
61
  function ResponsiveGridItem({
57
62
  span = 1,
63
+ rowSpan,
58
64
  children,
59
65
  className
60
66
  }) {
61
- const steps = typeof span === "number" ? { base: 1, sm: span, md: span, lg: span } : resolveColumns(span);
62
- const style = Object.fromEntries(
63
- Object.entries(steps).map(([step, value]) => [`--responsive-grid-item-${step}`, value])
64
- );
67
+ const style = Object.fromEntries([
68
+ ...Object.entries(resolveSpan(span)).map(([step, value]) => [
69
+ `--responsive-grid-item-${step}`,
70
+ value
71
+ ]),
72
+ // Only when asked: an item without `rowSpan` renders the style it always had.
73
+ ...rowSpan === void 0 ? [] : Object.entries(resolveSpan(rowSpan)).map(([step, value]) => [
74
+ `--responsive-grid-item-row-${step}`,
75
+ value
76
+ ])
77
+ ]);
65
78
  return /* @__PURE__ */ jsx("div", { className: cn("ui-responsive-grid-item", className), style, children });
66
79
  }
67
80
  ResponsiveGrid.Item = ResponsiveGridItem;
@@ -47,4 +47,4 @@ export { createSidebarLink } from "./sidebar-link.js";
47
47
  * Sidebar — data-driven vertical nav rail. Use {@link createSidebarLink} to adapt a router `Link`,
48
48
  * or `SidebarItem asChild` when composing rows by hand.
49
49
  */
50
- export declare function Sidebar({ ariaLabel: ariaLabelCamel, scrollRef, activeId, onSelect, sections, product, onProductClick, brand, collapsed, children, linkComponent, renderItem, footer, "aria-label": ariaLabel, }: SidebarProp): React.JSX.Element;
50
+ export declare function Sidebar({ ariaLabel: ariaLabelCamel, scrollRef, activeId, onSelect, sections, product, onProductClick, brand, action, collapsed, children, linkComponent, renderItem, footer, "aria-label": ariaLabel, }: SidebarProp): React.JSX.Element;
@@ -301,6 +301,7 @@ function Sidebar({
301
301
  product,
302
302
  onProductClick,
303
303
  brand,
304
+ action,
304
305
  collapsed = false,
305
306
  children,
306
307
  linkComponent,
@@ -314,6 +315,7 @@ function Sidebar({
314
315
  collapsed = surface === "drawer" ? false : collapsed;
315
316
  const brandNode = typeof brand === "function" ? brand(collapsed) : brand;
316
317
  const footerNode = typeof footer === "function" ? footer(collapsed) : footer;
318
+ const actionNode = typeof action === "function" ? action(collapsed) : action;
317
319
  return /* @__PURE__ */ jsxs("div", { className: "sb-root", "data-collapsed": collapsed ? "true" : void 0, children: [
318
320
  brand !== void 0 ? /* @__PURE__ */ jsx(SidebarHeader, { children: brandNode }) : product ? (() => {
319
321
  const interactive = onProductClick != null;
@@ -352,6 +354,7 @@ function Sidebar({
352
354
  meta
353
355
  ] });
354
356
  })() : null,
357
+ actionNode ? /* @__PURE__ */ jsx("div", { className: "sb-action", "data-slot": "sidebar-action", children: actionNode }) : null,
355
358
  /* @__PURE__ */ jsx(
356
359
  "nav",
357
360
  {
@@ -16,7 +16,7 @@ const TopbarItem = React.forwardRef(function TopbarItem2({
16
16
  ...props
17
17
  }, ref) {
18
18
  const Comp = asChild ? Slot : "button";
19
- const showBadge = !asChild && badge !== void 0 && badge !== "";
19
+ const showBadge = badge !== void 0 && badge !== "";
20
20
  const iconNode = icon !== void 0 && icon !== null && icon !== false ? /* @__PURE__ */ jsx(
21
21
  "span",
22
22
  {
@@ -35,7 +35,6 @@ const TopbarItem = React.forwardRef(function TopbarItem2({
35
35
  children: label
36
36
  }
37
37
  );
38
- const slotted = asChild && (iconNode !== null || labelHideBelow !== void 0) && React.isValidElement(children) ? React.cloneElement(children, void 0, iconNode, labelOf(children.props.children)) : children;
39
38
  const badgeNode = showBadge ? /* @__PURE__ */ jsx(
40
39
  "span",
41
40
  {
@@ -45,6 +44,13 @@ const TopbarItem = React.forwardRef(function TopbarItem2({
45
44
  children: badge
46
45
  }
47
46
  ) : null;
47
+ const slotted = asChild && (iconNode !== null || labelHideBelow !== void 0 || badgeNode !== null) && React.isValidElement(children) ? React.cloneElement(
48
+ children,
49
+ void 0,
50
+ iconNode,
51
+ labelOf(children.props.children),
52
+ badgeNode
53
+ ) : children;
48
54
  return /* @__PURE__ */ jsx(
49
55
  Comp,
50
56
  {
@@ -1,12 +1,14 @@
1
1
  import * as React from "react";
2
2
  import { type ImageLoadingStatus } from "../../lib/image-loading-status.js";
3
- import type { AvatarProp } from "../../props/components/data-display.prop.js";
3
+ import type { AvatarImageFitProp, AvatarProp } from "../../props/components/data-display.prop.js";
4
4
  export declare const Avatar: React.ForwardRefExoticComponent<Omit<React.DetailedHTMLProps<React.HTMLAttributes<HTMLSpanElement>, HTMLSpanElement>, "ref"> & {
5
5
  asChild?: boolean;
6
6
  } & Pick<AvatarProp, "appearance" | "presence" | "presenceLabel" | "shape" | "size"> & React.RefAttributes<HTMLSpanElement>>;
7
7
  export declare const AvatarImage: React.ForwardRefExoticComponent<Omit<React.DetailedHTMLProps<React.ImgHTMLAttributes<HTMLImageElement>, HTMLImageElement>, "ref"> & {
8
8
  asChild?: boolean;
9
9
  onLoadingStatusChange?: (status: ImageLoadingStatus) => void;
10
+ /** `cover` (default) crops to fill; `contain` shows a logo or app icon whole (gh#1176). */
11
+ fit?: AvatarImageFitProp;
10
12
  } & React.RefAttributes<HTMLImageElement>>;
11
13
  export declare const AvatarFallback: React.ForwardRefExoticComponent<Omit<React.DetailedHTMLProps<React.HTMLAttributes<HTMLSpanElement>, HTMLSpanElement>, "ref"> & {
12
14
  asChild?: boolean;
@@ -53,7 +53,7 @@ const Avatar = React.forwardRef(
53
53
  }
54
54
  );
55
55
  Avatar.displayName = "Avatar";
56
- const AvatarImage = React.forwardRef(({ className, src, onLoadingStatusChange, asChild = false, ...props }, ref) => {
56
+ const AvatarImage = React.forwardRef(({ className, src, onLoadingStatusChange, asChild = false, fit, ...props }, ref) => {
57
57
  const { imageLoadingStatus, setImageLoadingStatus } = useAvatarContext("AvatarImage");
58
58
  const status = useImageLoadingStatus(src, {
59
59
  referrerPolicy: props.referrerPolicy,
@@ -78,6 +78,7 @@ const AvatarImage = React.forwardRef(({ className, src, onLoadingStatusChange, a
78
78
  {
79
79
  ref,
80
80
  "data-slot": "avatar-image",
81
+ "data-fit": fit === "contain" ? "contain" : void 0,
81
82
  className: cn("ui-avatar-image", className),
82
83
  ...props,
83
84
  src
@@ -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": "31.28.3",
3
+ "version": "31.29.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,
@@ -232,6 +232,14 @@
232
232
  "symbols": "Symbols",
233
233
  "flags": "Flags"
234
234
  }
235
+ },
236
+ "sortableList": {
237
+ "handle": "Reorder {label}",
238
+ "hint": "Press Space to pick up, the arrow keys to move, Space to drop, Escape to cancel.",
239
+ "lifted": "Picked up {label}. Position {position} of {count}.",
240
+ "moved": "{label}: position {position} of {count}.",
241
+ "dropped": "Dropped {label} at position {position} of {count}.",
242
+ "cancelled": "Move cancelled. {label} is back at position {position} of {count}."
235
243
  }
236
244
  },
237
245
  "feedback": {
@@ -228,6 +228,14 @@
228
228
  "symbols": "記号",
229
229
  "flags": "旗"
230
230
  }
231
+ },
232
+ "sortableList": {
233
+ "handle": "{label} を並べ替え",
234
+ "hint": "Space で持ち上げ、矢印キーで移動、Space で確定、Esc で取り消します。",
235
+ "lifted": "{label} を持ち上げました。{count} 件中 {position} 番目です。",
236
+ "moved": "{label}: {count} 件中 {position} 番目。",
237
+ "dropped": "{label} を {count} 件中 {position} 番目に置きました。",
238
+ "cancelled": "移動を取り消しました。{label} は {count} 件中 {position} 番目に戻りました。"
231
239
  }
232
240
  },
233
241
  "feedback": {
@@ -229,6 +229,14 @@
229
229
  "symbols": "Ký hiệu",
230
230
  "flags": "Cờ"
231
231
  }
232
+ },
233
+ "sortableList": {
234
+ "handle": "Sắp xếp lại {label}",
235
+ "hint": "Nhấn Space để nhấc, phím mũi tên để di chuyển, Space để thả, Esc để huỷ.",
236
+ "lifted": "Đã nhấc {label}. Vị trí {position}/{count}.",
237
+ "moved": "{label}: vị trí {position}/{count}.",
238
+ "dropped": "Đã thả {label} ở vị trí {position}/{count}.",
239
+ "cancelled": "Đã huỷ di chuyển. {label} trở lại vị trí {position}/{count}."
232
240
  }
233
241
  },
234
242
  "feedback": {
@@ -303,6 +303,15 @@ export type AvatarPresenceProp = "online" | "away" | "busy" | "offline";
303
303
  * @see Avatar
304
304
  */
305
305
  export type AvatarAppearanceProp = "default" | "tinted";
306
+ /**
307
+ * How an `AvatarImage` fills its frame (`object-fit`). `cover` (default) fills the frame and crops
308
+ * — right for a photo of a face. `contain` shows the whole image inside the frame — right for a
309
+ * logo or an app icon that is wide, transparent or not centred, which `cover` would cut off
310
+ * (gh#1176). A full-bleed square icon (the GoDX service icon set) wants `cover`.
311
+ *
312
+ * @see Avatar
313
+ */
314
+ export type AvatarImageFitProp = "cover" | "contain";
306
315
  /** @see Badge */
307
316
  export type BadgeProp = {
308
317
  /**
@@ -2361,6 +2361,40 @@ export type AttachmentsRefProp = {
2361
2361
  multiple?: boolean;
2362
2362
  }) => void;
2363
2363
  };
2364
+ /** One row of a {@link SortableListProp}: its key (`value`), and the name the announcements speak. */
2365
+ export type SortableListItemProp = {
2366
+ /** Stable key — the order is reported as these keys. */
2367
+ value: string;
2368
+ /** Spoken in the handle's name and the live announcements; rendered when there is no `renderItem`. */
2369
+ label: string;
2370
+ /** This item's grip is inert; other items still move past it. */
2371
+ disabled?: boolean;
2372
+ };
2373
+ /**
2374
+ * @see SortableList — reorder a flat list or a grid of tiles (gh#1173): pointer drag on each
2375
+ * item's grip, or the keyboard (Space/Enter lift, arrows / Home / End move, Space/Enter drop, Esc
2376
+ * cancel), with every step announced politely ("2 / 3"). The order is the value.
2377
+ */
2378
+ export type SortableListProp = {
2379
+ items: SortableListItemProp[];
2380
+ /** The item keys in order. Controlled with `onValueChange`; keys not in `items` are ignored. */
2381
+ value?: ValueProp<string[]>;
2382
+ defaultValue?: DefaultValueProp<string[]>;
2383
+ /** Called once per drop with the keys in their new order (not on every preview step). */
2384
+ onValueChange?: OnValueChangeProp<string[]>;
2385
+ /** Draws an item's content beside its grip. Default: the item's `label`. */
2386
+ renderItem?: (item: SortableListItemProp, state: {
2387
+ index: number;
2388
+ dragging: boolean;
2389
+ }) => React.ReactNode;
2390
+ /** `list` stacks rows; `grid` flows tiles into auto-filled columns. Default `list`. */
2391
+ layout?: "list" | "grid";
2392
+ disabled?: DisabledProp;
2393
+ /** Accessible name of the list. */
2394
+ "aria-label"?: string;
2395
+ id?: IdProp;
2396
+ className?: ClassNameProp;
2397
+ };
2364
2398
  /**
2365
2399
  * @see EmojiPicker — a page icon / reaction picker (gh#1164): search by ja / en / vi keywords
2366
2400
  * (emojibase-data, loaded for the active locale on first open), categories, per-user recents, a