@lablup/ui-common 0.2.0-alpha.18 → 0.2.0-alpha.19

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 (83) hide show
  1. package/README.md +2 -2
  2. package/astryx/components/Form.doc.mjs +1 -1
  3. package/dist/assets/components/Form/FormItemVisual.css +1 -1
  4. package/dist/astryx/DateInput/tokens.stylex.d.ts +1 -0
  5. package/dist/astryx/DateInput/tokens.stylex.js +1 -0
  6. package/dist/astryx/Icon/IconSize.stylex.d.ts +1 -0
  7. package/dist/astryx/Icon/IconSize.stylex.js +1 -0
  8. package/dist/astryx/Indicator/indicator.markers.stylex.d.ts +1 -0
  9. package/dist/astryx/Indicator/indicator.markers.stylex.js +1 -0
  10. package/dist/astryx/Layer/layerAnimations.stylex.d.ts +1 -0
  11. package/dist/astryx/Layer/layerAnimations.stylex.js +1 -0
  12. package/dist/astryx/Layer/layerTextReset.stylex.d.ts +1 -0
  13. package/dist/astryx/Layer/layerTextReset.stylex.js +1 -0
  14. package/dist/astryx/Layer/layerViewportInset.stylex.d.ts +1 -0
  15. package/dist/astryx/Layer/layerViewportInset.stylex.js +1 -0
  16. package/dist/astryx/Layout/container.stylex.d.ts +1 -0
  17. package/dist/astryx/Layout/container.stylex.js +1 -0
  18. package/dist/astryx/Layout/edgeCompensation.stylex.d.ts +1 -0
  19. package/dist/astryx/Layout/edgeCompensation.stylex.js +1 -0
  20. package/dist/astryx/Layout/padding.stylex.d.ts +1 -0
  21. package/dist/astryx/Layout/padding.stylex.js +1 -0
  22. package/dist/astryx/Markdown/plugin-renderer.d.ts +1 -0
  23. package/dist/astryx/Markdown/plugin-renderer.js +1 -0
  24. package/dist/astryx/NavItem/navItemStyles.stylex.d.ts +1 -0
  25. package/dist/astryx/NavItem/navItemStyles.stylex.js +1 -0
  26. package/dist/astryx/Selector/selectorPresentation.stylex.d.ts +1 -0
  27. package/dist/astryx/Selector/selectorPresentation.stylex.js +1 -0
  28. package/dist/astryx/Stack/stack.stylex.d.ts +1 -0
  29. package/dist/astryx/Stack/stack.stylex.js +1 -0
  30. package/dist/astryx/Stack/stackItem.stylex.d.ts +1 -0
  31. package/dist/astryx/Stack/stackItem.stylex.js +1 -0
  32. package/dist/astryx/Text/text.stylex.d.ts +1 -0
  33. package/dist/astryx/Text/text.stylex.js +1 -0
  34. package/dist/astryx/theme/dataTokens.stylex.d.ts +1 -0
  35. package/dist/astryx/theme/dataTokens.stylex.js +1 -0
  36. package/dist/astryx/theme/neutral/fonts.css +3 -0
  37. package/dist/astryx/utils/focusOutline.stylex.d.ts +1 -0
  38. package/dist/astryx/utils/focusOutline.stylex.js +1 -0
  39. package/dist/astryx/utils/interactionOverlay.stylex.d.ts +1 -0
  40. package/dist/astryx/utils/interactionOverlay.stylex.js +1 -0
  41. package/dist/components/Form/FormItem.d.ts +5 -0
  42. package/dist/components/Form/FormItem.js +68 -67
  43. package/dist/components/Form/FormItem.js.map +1 -1
  44. package/dist/components/Form/FormItemVisual.d.ts +7 -0
  45. package/dist/components/Form/FormItemVisual2.js +60 -55
  46. package/dist/components/Form/FormItemVisual2.js.map +1 -1
  47. package/dist/components/NotificationStack/NotificationStack2.js +1 -0
  48. package/dist/components/NotificationStack/NotificationStack2.js.map +1 -1
  49. package/dist/components/PagedSelector/PagedSelector.d.ts +1 -1
  50. package/dist/components/PagedSelector/PagedSelector2.js.map +1 -1
  51. package/dist/forks/ComplexSelector/ComplexSelector.d.ts +82 -6
  52. package/dist/forks/ComplexSelector/ComplexSelector.js +170 -114
  53. package/dist/forks/ComplexSelector/ComplexSelector.js.map +1 -1
  54. package/dist/forks/ComplexSelector/ComplexSelector.styles.d.ts +40 -0
  55. package/dist/forks/ComplexSelector/ComplexSelector.styles.js +46 -6
  56. package/dist/forks/ComplexSelector/ComplexSelector.styles.js.map +1 -1
  57. package/dist/forks/ComplexSelector/index.d.ts +1 -1
  58. package/dist/forks/Drawer/Drawer.d.ts +21 -3
  59. package/dist/forks/Drawer/Drawer.js +70 -65
  60. package/dist/forks/Drawer/Drawer.js.map +1 -1
  61. package/dist/forks/Drawer/Drawer.styles.d.ts +16 -0
  62. package/dist/forks/Drawer/Drawer.styles.js +16 -0
  63. package/dist/forks/Drawer/Drawer.styles.js.map +1 -1
  64. package/dist/forks/Drawer/useDrawerDialogPresence.js.map +1 -1
  65. package/dist/forks/Tour/Tour.js +38 -40
  66. package/dist/forks/Tour/Tour.js.map +1 -1
  67. package/dist/forks/Tour/TourContext.js.map +1 -1
  68. package/dist/forks/Tour/TourStep.js.map +1 -1
  69. package/dist/forks/Tour/TourStep.styles.js.map +1 -1
  70. package/dist/forks/Tour/useTour.js.map +1 -1
  71. package/dist/index.d.ts +5 -5
  72. package/dist/index.js +2 -2
  73. package/dist/instance.js +2 -2
  74. package/dist/locales/en.generated.js +34 -0
  75. package/dist/locales/en.json +136 -0
  76. package/dist/locales/pseudo.generated.js +34 -0
  77. package/dist/locales/pseudo.json +102 -0
  78. package/dist/theme/lablup/built/lablup.d.ts +3 -3
  79. package/dist/theme/lablup/built/lablup.js +24 -5
  80. package/dist/theme/lablup/built/lablup.variants.d.ts +2 -2
  81. package/dist/theme/lablup/built/theme.css +70 -63
  82. package/dist/theme/lablup/built/theme.css.d.ts +9 -0
  83. package/package.json +86 -6
@@ -13,23 +13,24 @@ var o = (...e) => e.filter(Boolean).join(" "), s = 24, c = (e, t) => {
13
13
  n.maxWidth = r, t === "label" && (n.flex = `0 0 ${r}`);
14
14
  }
15
15
  return e.flex !== void 0 && (n.flex = typeof e.flex == "number" ? `${e.flex} ${e.flex} auto` : e.flex), e.offset !== void 0 && (n.marginInlineStart = `${e.offset / s * 100}%`), n;
16
- }, l = ({ label: s, labelTitle: l, tooltip: u, tooltipIcon: d, extra: f, help: p, required: m, requiredMarkType: h, layout: g = "vertical", size: _, colon: v, labelAlign: y, labelCol: b, wrapperCol: x, labelWrap: S, errors: C = [], warnings: w = [], status: T, hasFeedback: E, fieldId: D, fieldHandle: O, htmlFor: k, className: A, style: j, hidden: M, children: N }) => {
17
- let P = p != null && p !== !1, F = P || C.length > 0 || w.length > 0, I = t.useRef(null), L = t.useRef(null), [R, z] = t.useState(null), [B, V] = t.useState(0);
16
+ }, l = ({ label: s, labelTitle: l, tooltip: u, tooltipIcon: d, labelExtra: f, extra: p, help: m, required: h, requiredMarkType: g, layout: _ = "vertical", size: v, colon: y, labelAlign: b, labelCol: x, wrapperCol: S, labelWrap: C, errors: w = [], warnings: T = [], status: E, hasFeedback: D, fieldId: O, fieldHandle: k, htmlFor: A, className: j, style: M, hidden: N, children: P }) => {
17
+ let F = m != null && m !== !1, I = F || w.length > 0 || T.length > 0, L = t.useRef(null), R = t.useRef(null), [z, B] = t.useState(null), [V, H] = t.useState(0);
18
18
  t.useLayoutEffect(() => {
19
- F && I.current ? z(Number.parseInt(getComputedStyle(I.current).marginBottom, 10) || 0) : z(null);
20
- }, [F]), t.useLayoutEffect(() => {
21
- V(f && L.current ? L.current.clientHeight : 0);
22
- }, [f]);
23
- let H = s == null ? null : /* @__PURE__ */ n("div", {
24
- className: o("uic-form-item__label-col", b?.className),
25
- "data-align": y === "left" ? "left" : void 0,
26
- "data-wrap": S ? "" : void 0,
27
- style: c(b, "label"),
28
- children: /* @__PURE__ */ r("label", {
29
- htmlFor: k ?? D,
30
- className: o("uic-form-item__label", m && "uic-form-item__label--required"),
31
- "data-required-mark": h,
32
- "data-no-colon": v === !1 ? "" : void 0,
19
+ I && L.current ? B(Number.parseInt(getComputedStyle(L.current).marginBottom, 10) || 0) : B(null);
20
+ }, [I]), t.useLayoutEffect(() => {
21
+ H(p && R.current ? R.current.clientHeight : 0);
22
+ }, [p]);
23
+ let U = f != null && f !== !1, W = s == null ? null : /* @__PURE__ */ r("div", {
24
+ className: o("uic-form-item__label-col", x?.className),
25
+ "data-align": b === "left" ? "left" : void 0,
26
+ "data-wrap": C ? "" : void 0,
27
+ "data-has-label-extra": U ? "" : void 0,
28
+ style: c(x, "label"),
29
+ children: [/* @__PURE__ */ r("label", {
30
+ htmlFor: A ?? O,
31
+ className: o("uic-form-item__label", h && "uic-form-item__label--required"),
32
+ "data-required-mark": g,
33
+ "data-no-colon": y === !1 ? "" : void 0,
33
34
  title: l ?? (typeof s == "string" ? s : void 0),
34
35
  children: [s, u ? /* @__PURE__ */ n(i, {
35
36
  content: u,
@@ -42,71 +43,75 @@ var o = (...e) => e.filter(Boolean).join(" "), s = 24, c = (e, t) => {
42
43
  children: d ?? /* @__PURE__ */ n(a, { size: "1em" })
43
44
  })
44
45
  }) : null]
45
- })
46
- }), U = P ? [{
46
+ }), U ? /* @__PURE__ */ n("span", {
47
+ className: "uic-form-item__label-extra",
48
+ "data-no-colon": y === !1 ? "" : void 0,
49
+ children: f
50
+ }) : null]
51
+ }), G = F ? [{
47
52
  key: "help",
48
- node: p,
49
- kind: T === "error" ? "error" : T === "warning" ? "warning" : null
50
- }] : [...C.map((e, t) => ({
53
+ node: m,
54
+ kind: E === "error" ? "error" : E === "warning" ? "warning" : null
55
+ }] : [...w.map((e, t) => ({
51
56
  key: `e-${t}`,
52
57
  node: e,
53
58
  kind: "error"
54
- })), ...w.map((e, t) => ({
59
+ })), ...T.map((e, t) => ({
55
60
  key: `w-${t}`,
56
61
  node: e,
57
62
  kind: "warning"
58
- }))], W = U.length ? /* @__PURE__ */ n("div", {
59
- id: D ? `${D}_help` : void 0,
63
+ }))], K = G.length ? /* @__PURE__ */ n("div", {
64
+ id: O ? `${O}_help` : void 0,
60
65
  className: "uic-form-item__explain",
61
- children: U.map((e) => /* @__PURE__ */ n("div", {
66
+ children: G.map((e) => /* @__PURE__ */ n("div", {
62
67
  className: e.kind ? `uic-form-item__explain-${e.kind}` : void 0,
63
68
  children: e.node
64
69
  }, e.key))
65
- }) : null, G = f ? /* @__PURE__ */ n("div", {
66
- ref: L,
67
- id: D ? `${D}_extra` : void 0,
70
+ }) : null, q = p ? /* @__PURE__ */ n("div", {
71
+ ref: R,
72
+ id: O ? `${O}_extra` : void 0,
68
73
  className: "uic-form-item__extra",
69
- children: f
70
- }) : null, K = E && T ? /* @__PURE__ */ n("span", {
74
+ children: p
75
+ }) : null, J = D && E ? /* @__PURE__ */ n("span", {
71
76
  className: "uic-form-item__feedback-icon",
72
- "data-status": T,
73
- children: e(T)
74
- }) : null, q = /* @__PURE__ */ r("div", {
75
- className: o("uic-form-item__control", x?.className),
76
- "data-span": x?.span === void 0 ? void 0 : "",
77
- style: c(x, "control"),
77
+ "data-status": E,
78
+ children: e(E)
79
+ }) : null, Y = /* @__PURE__ */ r("div", {
80
+ className: o("uic-form-item__control", S?.className),
81
+ "data-span": S?.span === void 0 ? void 0 : "",
82
+ style: c(S, "control"),
78
83
  children: [/* @__PURE__ */ r("div", {
79
84
  className: "uic-form-item__control-input",
80
85
  children: [/* @__PURE__ */ n("div", {
81
86
  className: "uic-form-item__control-input-content",
82
- children: N
83
- }), K]
84
- }), W || G ? /* @__PURE__ */ r("div", {
87
+ children: P
88
+ }), J]
89
+ }), K || q ? /* @__PURE__ */ r("div", {
85
90
  className: "uic-form-item__additional",
86
- style: R ? { minHeight: R + B } : void 0,
87
- children: [W, G]
91
+ style: z ? { minHeight: z + V } : void 0,
92
+ children: [K, q]
88
93
  }) : null]
89
94
  });
90
95
  return /* @__PURE__ */ r("div", {
91
- ref: I,
92
- className: o("uic-form-item", A),
93
- "data-uic-field-item": O,
94
- "data-layout": g,
95
- "data-size": _,
96
- "data-status": T || void 0,
97
- "data-feedback": K ? "" : void 0,
98
- "data-hidden": M ? "" : void 0,
99
- style: M ? {
96
+ ref: L,
97
+ className: o("uic-form-item", j),
98
+ "data-uic-field-item": k,
99
+ "data-layout": _,
100
+ "data-size": v,
101
+ "data-status": E || void 0,
102
+ "data-feedback": J ? "" : void 0,
103
+ "data-hidden": N ? "" : void 0,
104
+ style: N ? {
100
105
  display: "none",
101
- ...j
102
- } : j,
106
+ ...M
107
+ } : M,
103
108
  children: [/* @__PURE__ */ r("div", {
104
109
  className: "uic-form-item__row",
105
- children: [H, q]
106
- }), R ? /* @__PURE__ */ n("div", {
110
+ children: [W, Y]
111
+ }), z ? /* @__PURE__ */ n("div", {
107
112
  "aria-hidden": !0,
108
113
  className: "uic-form-item__margin-offset",
109
- style: { marginBottom: -R }
114
+ style: { marginBottom: -z }
110
115
  }) : null]
111
116
  });
112
117
  };
@@ -1 +1 @@
1
- {"version":3,"file":"FormItemVisual2.js","names":[],"sources":["../../../src/components/Form/FormItemVisual.tsx"],"sourcesContent":["/**\n * The presentational shell of a form item: label, control, explain and extra,\n * laid out the way antd lays out `Form.Item` (`layout`, `labelCol` /\n * `wrapperCol` spans, `colon`, `labelAlign`, `labelWrap`, `size`,\n * `hasFeedback`, `validateStatus`), so a form moved onto the engine keeps its\n * geometry. Styles are in `FormItemVisual.css`.\n *\n * .uic-form-item\n * .uic-form-item__row\n * .uic-form-item__label-col\n * label.uic-form-item__label (+ .uic-form-item__tooltip)\n * .uic-form-item__control\n * .uic-form-item__control-input\n * .uic-form-item__control-input-content\n * .uic-form-item__feedback-icon\n * .uic-form-item__additional\n * .uic-form-item__explain (.uic-form-item__explain-error / -warning rows)\n * .uic-form-item__extra\n * .uic-form-item__margin-offset\n */\nimport { CircleHelp } from \"lucide-react\";\nimport * as React from \"react\";\nimport { Tooltip } from \"@astryxdesign/core/Tooltip\";\n\nimport getFeedbackIcon from \"./feedbackIcons\";\nimport \"./FormItemVisual.css\";\n\nexport type FormItemLayout = \"vertical\" | \"horizontal\" | \"inline\";\nexport type FormItemSize = \"small\" | \"middle\" | \"large\";\nexport type FormItemStatus = \"success\" | \"warning\" | \"error\" | \"validating\" | \"\";\n\n/** antd's `Col` props, reduced to `span`, `offset`, `flex`, `className` and `style`. */\nexport interface FormItemCol {\n span?: number;\n offset?: number;\n flex?: string | number;\n className?: string;\n style?: React.CSSProperties;\n}\n\nexport interface FormItemVisualProps {\n label?: React.ReactNode;\n /**\n * `<label title>`. Separate from `label` because it is taken from the\n * original prop, before `requiredMark` wraps it: a function mark turns the\n * label into an element and the title would disappear.\n */\n labelTitle?: string;\n /** The tooltip body, rendered behind a help glyph after the label. */\n tooltip?: React.ReactNode;\n /** The tooltip trigger glyph. Defaults to a question mark in a circle. */\n tooltipIcon?: React.ReactNode;\n extra?: React.ReactNode;\n help?: React.ReactNode;\n /** Renders the required marker. Independent of the `required` rule. */\n required?: boolean;\n /**\n * How the marker is drawn: `'optional'` and `'hidden'` hide the asterisk (a\n * function or `'optional'` `requiredMark` puts the hint in the label).\n */\n requiredMarkType?: \"optional\" | \"hidden\";\n layout?: FormItemLayout;\n size?: FormItemSize;\n colon?: boolean;\n labelAlign?: \"left\" | \"right\";\n labelCol?: FormItemCol;\n wrapperCol?: FormItemCol;\n labelWrap?: boolean;\n errors?: React.ReactNode[];\n warnings?: React.ReactNode[];\n /** Merged validation status; drives colours, the feedback icon and controls. */\n status?: FormItemStatus;\n hasFeedback?: boolean;\n fieldId?: string;\n /** Same handle as the control's `data-uic-field-id`, for a child that forwards nothing to the DOM. */\n fieldHandle?: string;\n htmlFor?: string;\n className?: string;\n style?: React.CSSProperties;\n hidden?: boolean;\n children?: React.ReactNode;\n}\n\nconst cx = (...names: (string | false | undefined)[]) =>\n names.filter(Boolean).join(\" \");\n\n/**\n * antd grid: `span` / `offset` are 24ths. On the label a span is a fixed\n * basis; on the control it is only a max-width, because antd's horizontal\n * control rule (`flex: 1 1 0`) outranks the column's basis.\n */\nconst GRID_MAX = 24;\nconst colStyle = (\n col: FormItemCol | undefined,\n role: \"label\" | \"control\",\n): React.CSSProperties | undefined => {\n if (!col) return undefined;\n const style: React.CSSProperties = { ...col.style };\n if (col.span !== undefined) {\n const pct = `${(col.span / GRID_MAX) * 100}%`;\n style.maxWidth = pct;\n if (role === \"label\") style.flex = `0 0 ${pct}`;\n }\n if (col.flex !== undefined) {\n style.flex =\n typeof col.flex === \"number\" ? `${col.flex} ${col.flex} auto` : col.flex;\n }\n if (col.offset !== undefined) {\n style.marginInlineStart = `${(col.offset / GRID_MAX) * 100}%`;\n }\n return style;\n};\n\nexport const FormItemVisual: React.FC<FormItemVisualProps> = ({\n label,\n labelTitle,\n tooltip,\n tooltipIcon,\n extra,\n help,\n required,\n requiredMarkType,\n layout = \"vertical\",\n size,\n colon,\n labelAlign,\n labelCol,\n wrapperCol,\n labelWrap,\n errors = [],\n warnings = [],\n status,\n hasFeedback,\n fieldId,\n fieldHandle,\n htmlFor,\n className,\n style,\n hidden,\n children,\n}) => {\n const hasHelp = help !== undefined && help !== null && help !== false;\n // antd's trigger for the reserved-space / margin-offset pair.\n const hasExplain = hasHelp || errors.length > 0 || warnings.length > 0;\n\n /**\n * antd reserves the item's own computed `margin-bottom` inside the\n * additional block while an explain block shows, and cancels it with a\n * negative-margin spacer, so one line of error causes no layout jump. The\n * computed value is read, so a `style.marginBottom` override still nets out.\n */\n const itemRef = React.useRef<HTMLDivElement>(null);\n const extraRef = React.useRef<HTMLDivElement>(null);\n const [marginBottom, setMarginBottom] = React.useState<number | null>(null);\n const [extraHeight, setExtraHeight] = React.useState(0);\n\n React.useLayoutEffect(() => {\n if (hasExplain && itemRef.current) {\n setMarginBottom(\n Number.parseInt(getComputedStyle(itemRef.current).marginBottom, 10) || 0,\n );\n } else {\n setMarginBottom(null);\n }\n }, [hasExplain]);\n\n React.useLayoutEffect(() => {\n setExtraHeight(extra && extraRef.current ? extraRef.current.clientHeight : 0);\n }, [extra]);\n\n const labelNode =\n label === undefined || label === null ? null : (\n <div\n className={cx(\"uic-form-item__label-col\", labelCol?.className)}\n data-align={labelAlign === \"left\" ? \"left\" : undefined}\n data-wrap={labelWrap ? \"\" : undefined}\n style={colStyle(labelCol, \"label\")}\n >\n <label\n htmlFor={htmlFor ?? fieldId}\n className={cx(\n \"uic-form-item__label\",\n required && \"uic-form-item__label--required\",\n )}\n data-required-mark={requiredMarkType}\n // The colon is drawn in every layout and hidden in vertical, as in\n // antd, so a vertical label measures the same.\n data-no-colon={colon === false ? \"\" : undefined}\n title={labelTitle ?? (typeof label === \"string\" ? label : undefined)}\n >\n {label}\n {tooltip ? (\n <Tooltip content={tooltip}>\n <span\n className=\"uic-form-item__tooltip\"\n role=\"button\"\n tabIndex={0}\n aria-label={typeof label === \"string\" ? `${label} — info` : \"info\"}\n // The label is a `<label htmlFor>`: a click inside it would\n // move focus to the control the moment the hint is tapped.\n onClick={(e) => e.preventDefault()}\n >\n {tooltipIcon ?? <CircleHelp size=\"1em\" />}\n </span>\n </Tooltip>\n ) : null}\n </label>\n </div>\n );\n\n // antd's `ErrorList`: `help` replaces the error/warning list and takes the\n // item's status, so `validateStatus=\"error\" help=\"…\"` paints the help red.\n const helpKind =\n status === \"error\" ? \"error\" : status === \"warning\" ? \"warning\" : null;\n const explainRows: Array<{\n key: string;\n node: React.ReactNode;\n kind: \"error\" | \"warning\" | null;\n }> = hasHelp\n ? [{ key: \"help\", node: help, kind: helpKind }]\n : [\n ...errors.map((e, i) => ({ key: `e-${i}`, node: e, kind: \"error\" as const })),\n ...warnings.map((w, i) => ({\n key: `w-${i}`,\n node: w,\n kind: \"warning\" as const,\n })),\n ];\n\n const explainNode = explainRows.length ? (\n <div\n id={fieldId ? `${fieldId}_help` : undefined}\n className=\"uic-form-item__explain\"\n >\n {explainRows.map((row) => (\n <div\n key={row.key}\n className={row.kind ? `uic-form-item__explain-${row.kind}` : undefined}\n >\n {row.node}\n </div>\n ))}\n </div>\n ) : null;\n\n const extraNode = extra ? (\n <div\n ref={extraRef}\n id={fieldId ? `${fieldId}_extra` : undefined}\n className=\"uic-form-item__extra\"\n >\n {extra}\n </div>\n ) : null;\n\n const feedbackNode =\n hasFeedback && status ? (\n <span className=\"uic-form-item__feedback-icon\" data-status={status}>\n {getFeedbackIcon(status)}\n </span>\n ) : null;\n\n const control = (\n <div\n className={cx(\"uic-form-item__control\", wrapperCol?.className)}\n data-span={wrapperCol?.span !== undefined ? \"\" : undefined}\n style={colStyle(wrapperCol, \"control\")}\n >\n <div className=\"uic-form-item__control-input\">\n <div className=\"uic-form-item__control-input-content\">{children}</div>\n {feedbackNode}\n </div>\n {explainNode || extraNode ? (\n <div\n className=\"uic-form-item__additional\"\n style={marginBottom ? { minHeight: marginBottom + extraHeight } : undefined}\n >\n {explainNode}\n {extraNode}\n </div>\n ) : null}\n </div>\n );\n\n return (\n <div\n ref={itemRef}\n className={cx(\"uic-form-item\", className)}\n data-uic-field-item={fieldHandle}\n data-layout={layout}\n data-size={size}\n data-status={status || undefined}\n data-feedback={feedbackNode ? \"\" : undefined}\n data-hidden={hidden ? \"\" : undefined}\n // `display: none` inline too: hiding a field is a correctness property\n // and must hold even without the stylesheet.\n style={hidden ? { display: \"none\", ...style } : style}\n >\n <div className=\"uic-form-item__row\">\n {labelNode}\n {control}\n </div>\n {marginBottom ? (\n <div\n aria-hidden\n className=\"uic-form-item__margin-offset\"\n style={{ marginBottom: -marginBottom }}\n />\n ) : null}\n </div>\n );\n};\n\nexport default FormItemVisual;\n"],"mappings":"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"}
1
+ {"version":3,"file":"FormItemVisual2.js","names":[],"sources":["../../../src/components/Form/FormItemVisual.tsx"],"sourcesContent":["/**\n * The presentational shell of a form item: label, control, explain and extra,\n * laid out the way antd lays out `Form.Item` (`layout`, `labelCol` /\n * `wrapperCol` spans, `colon`, `labelAlign`, `labelWrap`, `size`,\n * `hasFeedback`, `validateStatus`), so a form moved onto the engine keeps its\n * geometry. Styles are in `FormItemVisual.css`.\n *\n * .uic-form-item\n * .uic-form-item__row\n * .uic-form-item__label-col\n * label.uic-form-item__label (+ .uic-form-item__tooltip)\n * .uic-form-item__label-extra\n * .uic-form-item__control\n * .uic-form-item__control-input\n * .uic-form-item__control-input-content\n * .uic-form-item__feedback-icon\n * .uic-form-item__additional\n * .uic-form-item__explain (.uic-form-item__explain-error / -warning rows)\n * .uic-form-item__extra\n * .uic-form-item__margin-offset\n */\nimport { CircleHelp } from \"lucide-react\";\nimport * as React from \"react\";\nimport { Tooltip } from \"@astryxdesign/core/Tooltip\";\n\nimport getFeedbackIcon from \"./feedbackIcons\";\nimport \"./FormItemVisual.css\";\n\nexport type FormItemLayout = \"vertical\" | \"horizontal\" | \"inline\";\nexport type FormItemSize = \"small\" | \"middle\" | \"large\";\nexport type FormItemStatus = \"success\" | \"warning\" | \"error\" | \"validating\" | \"\";\n\n/** antd's `Col` props, reduced to `span`, `offset`, `flex`, `className` and `style`. */\nexport interface FormItemCol {\n span?: number;\n offset?: number;\n flex?: string | number;\n className?: string;\n style?: React.CSSProperties;\n}\n\nexport interface FormItemVisualProps {\n label?: React.ReactNode;\n /**\n * `<label title>`. Separate from `label` because it is taken from the\n * original prop, before `requiredMark` wraps it: a function mark turns the\n * label into an element and the title would disappear.\n */\n labelTitle?: string;\n /** The tooltip body, rendered behind a help glyph after the label. */\n tooltip?: React.ReactNode;\n /** The tooltip trigger glyph. Defaults to a question mark in a circle. */\n tooltipIcon?: React.ReactNode;\n /**\n * Actions that belong to the label row (e.g. a filter button). Rendered\n * outside the `<label>`, so they stay out of the control's accessible name:\n * at the far end of the row in vertical layout, right after the label\n * otherwise. Needs a `label`.\n */\n labelExtra?: React.ReactNode;\n extra?: React.ReactNode;\n help?: React.ReactNode;\n /** Renders the required marker. Independent of the `required` rule. */\n required?: boolean;\n /**\n * How the marker is drawn: `'optional'` and `'hidden'` hide the asterisk (a\n * function or `'optional'` `requiredMark` puts the hint in the label).\n */\n requiredMarkType?: \"optional\" | \"hidden\";\n layout?: FormItemLayout;\n size?: FormItemSize;\n colon?: boolean;\n labelAlign?: \"left\" | \"right\";\n labelCol?: FormItemCol;\n wrapperCol?: FormItemCol;\n labelWrap?: boolean;\n errors?: React.ReactNode[];\n warnings?: React.ReactNode[];\n /** Merged validation status; drives colours, the feedback icon and controls. */\n status?: FormItemStatus;\n hasFeedback?: boolean;\n fieldId?: string;\n /** Same handle as the control's `data-uic-field-id`, for a child that forwards nothing to the DOM. */\n fieldHandle?: string;\n htmlFor?: string;\n className?: string;\n style?: React.CSSProperties;\n hidden?: boolean;\n children?: React.ReactNode;\n}\n\nconst cx = (...names: (string | false | undefined)[]) =>\n names.filter(Boolean).join(\" \");\n\n/**\n * antd grid: `span` / `offset` are 24ths. On the label a span is a fixed\n * basis; on the control it is only a max-width, because antd's horizontal\n * control rule (`flex: 1 1 0`) outranks the column's basis.\n */\nconst GRID_MAX = 24;\nconst colStyle = (\n col: FormItemCol | undefined,\n role: \"label\" | \"control\",\n): React.CSSProperties | undefined => {\n if (!col) return undefined;\n const style: React.CSSProperties = { ...col.style };\n if (col.span !== undefined) {\n const pct = `${(col.span / GRID_MAX) * 100}%`;\n style.maxWidth = pct;\n if (role === \"label\") style.flex = `0 0 ${pct}`;\n }\n if (col.flex !== undefined) {\n style.flex =\n typeof col.flex === \"number\" ? `${col.flex} ${col.flex} auto` : col.flex;\n }\n if (col.offset !== undefined) {\n style.marginInlineStart = `${(col.offset / GRID_MAX) * 100}%`;\n }\n return style;\n};\n\nexport const FormItemVisual: React.FC<FormItemVisualProps> = ({\n label,\n labelTitle,\n tooltip,\n tooltipIcon,\n labelExtra,\n extra,\n help,\n required,\n requiredMarkType,\n layout = \"vertical\",\n size,\n colon,\n labelAlign,\n labelCol,\n wrapperCol,\n labelWrap,\n errors = [],\n warnings = [],\n status,\n hasFeedback,\n fieldId,\n fieldHandle,\n htmlFor,\n className,\n style,\n hidden,\n children,\n}) => {\n const hasHelp = help !== undefined && help !== null && help !== false;\n // antd's trigger for the reserved-space / margin-offset pair.\n const hasExplain = hasHelp || errors.length > 0 || warnings.length > 0;\n\n /**\n * antd reserves the item's own computed `margin-bottom` inside the\n * additional block while an explain block shows, and cancels it with a\n * negative-margin spacer, so one line of error causes no layout jump. The\n * computed value is read, so a `style.marginBottom` override still nets out.\n */\n const itemRef = React.useRef<HTMLDivElement>(null);\n const extraRef = React.useRef<HTMLDivElement>(null);\n const [marginBottom, setMarginBottom] = React.useState<number | null>(null);\n const [extraHeight, setExtraHeight] = React.useState(0);\n\n React.useLayoutEffect(() => {\n if (hasExplain && itemRef.current) {\n setMarginBottom(\n Number.parseInt(getComputedStyle(itemRef.current).marginBottom, 10) || 0,\n );\n } else {\n setMarginBottom(null);\n }\n }, [hasExplain]);\n\n React.useLayoutEffect(() => {\n setExtraHeight(extra && extraRef.current ? extraRef.current.clientHeight : 0);\n }, [extra]);\n\n const hasLabelExtra =\n labelExtra !== undefined && labelExtra !== null && labelExtra !== false;\n\n const labelNode =\n label === undefined || label === null ? null : (\n <div\n className={cx(\"uic-form-item__label-col\", labelCol?.className)}\n data-align={labelAlign === \"left\" ? \"left\" : undefined}\n data-wrap={labelWrap ? \"\" : undefined}\n data-has-label-extra={hasLabelExtra ? \"\" : undefined}\n style={colStyle(labelCol, \"label\")}\n >\n <label\n htmlFor={htmlFor ?? fieldId}\n className={cx(\n \"uic-form-item__label\",\n required && \"uic-form-item__label--required\",\n )}\n data-required-mark={requiredMarkType}\n // The colon is drawn in every layout and hidden in vertical, as in\n // antd, so a vertical label measures the same.\n data-no-colon={colon === false ? \"\" : undefined}\n title={labelTitle ?? (typeof label === \"string\" ? label : undefined)}\n >\n {label}\n {tooltip ? (\n <Tooltip content={tooltip}>\n <span\n className=\"uic-form-item__tooltip\"\n role=\"button\"\n tabIndex={0}\n aria-label={typeof label === \"string\" ? `${label} — info` : \"info\"}\n // The label is a `<label htmlFor>`: a click inside it would\n // move focus to the control the moment the hint is tapped.\n onClick={(e) => e.preventDefault()}\n >\n {tooltipIcon ?? <CircleHelp size=\"1em\" />}\n </span>\n </Tooltip>\n ) : null}\n </label>\n {hasLabelExtra ? (\n // The colon moves here so it still ends the row in horizontal layout.\n <span\n className=\"uic-form-item__label-extra\"\n data-no-colon={colon === false ? \"\" : undefined}\n >\n {labelExtra}\n </span>\n ) : null}\n </div>\n );\n\n // antd's `ErrorList`: `help` replaces the error/warning list and takes the\n // item's status, so `validateStatus=\"error\" help=\"…\"` paints the help red.\n const helpKind =\n status === \"error\" ? \"error\" : status === \"warning\" ? \"warning\" : null;\n const explainRows: Array<{\n key: string;\n node: React.ReactNode;\n kind: \"error\" | \"warning\" | null;\n }> = hasHelp\n ? [{ key: \"help\", node: help, kind: helpKind }]\n : [\n ...errors.map((e, i) => ({ key: `e-${i}`, node: e, kind: \"error\" as const })),\n ...warnings.map((w, i) => ({\n key: `w-${i}`,\n node: w,\n kind: \"warning\" as const,\n })),\n ];\n\n const explainNode = explainRows.length ? (\n <div\n id={fieldId ? `${fieldId}_help` : undefined}\n className=\"uic-form-item__explain\"\n >\n {explainRows.map((row) => (\n <div\n key={row.key}\n className={row.kind ? `uic-form-item__explain-${row.kind}` : undefined}\n >\n {row.node}\n </div>\n ))}\n </div>\n ) : null;\n\n const extraNode = extra ? (\n <div\n ref={extraRef}\n id={fieldId ? `${fieldId}_extra` : undefined}\n className=\"uic-form-item__extra\"\n >\n {extra}\n </div>\n ) : null;\n\n const feedbackNode =\n hasFeedback && status ? (\n <span className=\"uic-form-item__feedback-icon\" data-status={status}>\n {getFeedbackIcon(status)}\n </span>\n ) : null;\n\n const control = (\n <div\n className={cx(\"uic-form-item__control\", wrapperCol?.className)}\n data-span={wrapperCol?.span !== undefined ? \"\" : undefined}\n style={colStyle(wrapperCol, \"control\")}\n >\n <div className=\"uic-form-item__control-input\">\n <div className=\"uic-form-item__control-input-content\">{children}</div>\n {feedbackNode}\n </div>\n {explainNode || extraNode ? (\n <div\n className=\"uic-form-item__additional\"\n style={marginBottom ? { minHeight: marginBottom + extraHeight } : undefined}\n >\n {explainNode}\n {extraNode}\n </div>\n ) : null}\n </div>\n );\n\n return (\n <div\n ref={itemRef}\n className={cx(\"uic-form-item\", className)}\n data-uic-field-item={fieldHandle}\n data-layout={layout}\n data-size={size}\n data-status={status || undefined}\n data-feedback={feedbackNode ? \"\" : undefined}\n data-hidden={hidden ? \"\" : undefined}\n // `display: none` inline too: hiding a field is a correctness property\n // and must hold even without the stylesheet.\n style={hidden ? { display: \"none\", ...style } : style}\n >\n <div className=\"uic-form-item__row\">\n {labelNode}\n {control}\n </div>\n {marginBottom ? (\n <div\n aria-hidden\n className=\"uic-form-item__margin-offset\"\n style={{ marginBottom: -marginBottom }}\n />\n ) : null}\n </div>\n );\n};\n\nexport default FormItemVisual;\n"],"mappings":"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"}
@@ -68,6 +68,7 @@ function v({ item: t, isExiting: n, onClose: a }) {
68
68
  children: t.title
69
69
  }),
70
70
  icon: t.icon,
71
+ role: t.status === "error" || t.status === "warning" ? "alert" : "status",
71
72
  elevation: "high",
72
73
  isDismissable: t.isClosable ?? !0,
73
74
  onDismiss: () => a?.(_),
@@ -1 +1 @@
1
- {"version":3,"file":"NotificationStack2.js","names":[],"sources":["../../../src/components/NotificationStack/NotificationStack.tsx"],"sourcesContent":["/**\n * NotificationStack\n *\n * Floating notices stacked in the bottom-end corner, newest nearest the\n * corner. Each notice is an Astryx `Banner` that can carry what a toast\n * cannot: background-task progress (determinate or not), Cancel / Retry and a\n * navigation action, a collapsible detail, and a body of the caller's own.\n *\n * It is presentational. The caller owns the list: it adds notices, removes\n * them in `onClose`, and updates a notice in place under the same `key`.\n *\n * - `duration` closes a notice after that many seconds; the countdown pauses\n * while the notice is hovered or holds focus. `null` or `0` keeps it open.\n * - An error notice opens its detail (`children`) up front; the reader's own\n * toggle wins after that.\n * - `maxVisible` caps how many render; the newest win, and the rest render as\n * room frees up.\n * - Removed notices slide out before they unmount, unless the reader asked\n * for reduced motion.\n * - A long description scrolls inside its notice, so the dismiss button stays\n * on screen; the stack scrolls once it reaches the top inset.\n *\n * Layout hooks, set on the stack or an ancestor:\n * `--notification-stack-z` (stacking order, default 11000, one above\n * `Modal`'s default band), `--notification-stack-inset-top` (space kept\n * free above the stack, such as an app header, default 0) and\n * `--notification-stack-body-max-height` (one notice's scroll cap, default 30vh).\n *\n * e2e hooks: each notice carries `data-notification-key`, `data-status` and\n * `data-paused`; its title and description carry `data-testid`\n * `notification-title` and `notification-description`.\n *\n * @example\n * <NotificationStack\n * notifications={[{ key: \"upload\", title: \"Uploading\", percent: 40 }]}\n * onClose={(key) => remove(key)}\n * />\n */\nimport {\n useEffect,\n useEffectEvent,\n useLayoutEffect,\n useMemo,\n useRef,\n useState,\n type Key,\n type ReactNode,\n} from \"react\";\nimport { Banner, type BannerStatus } from \"@astryxdesign/core/Banner\";\nimport { Button } from \"@astryxdesign/core/Button\";\nimport { ProgressBar } from \"@astryxdesign/core/ProgressBar\";\nimport { HStack, VStack } from \"@astryxdesign/core/Stack\";\nimport { Text } from \"@astryxdesign/core/Text\";\n\nimport { useUicTranslator } from \"../../i18n/useUicTranslator\";\nimport { MODAL_LIVE_ATTRIBUTE, refreshModalBackground } from \"../Modal/modalStack\";\nimport \"./NotificationStack.css\";\n\n/** Matches the exit animation's budget in NotificationStack.css. */\nconst EXIT_ANIMATION_MS = 200;\n\nexport interface NotificationStackItem {\n key: Key;\n /** Headline. */\n title: ReactNode;\n description?: ReactNode;\n /** Drives the Banner's icon and colour. @default 'info' */\n status?: BannerStatus;\n /** Background-task progress, 0-100. Omit for a notice with no task. */\n percent?: number;\n /** A task that is running with no measurable progress yet. */\n isProgressIndeterminate?: boolean;\n /**\n * Accessible name of the progress bar.\n * @default the title when it is a string, else the catalog's uic.NotificationStack.progress\n */\n progressLabel?: string;\n /** Label of the navigation action (\"View folder\"). Shown with `onAction`. */\n actionText?: string;\n onAction?: () => void;\n /** @default the catalog's uic.common.retry (\"Retry\") */\n retryText?: string;\n onRetry?: () => void;\n /** @default the catalog's uic.common.cancel (\"Cancel\") */\n cancelText?: string;\n onCancel?: () => void;\n /**\n * Seconds until the notice closes itself; `null` or `0` keeps it open until\n * dismissed. The countdown pauses while the notice is hovered or focused.\n */\n duration?: number | null;\n /** @default true */\n isClosable?: boolean;\n /** Overrides the status icon. */\n icon?: ReactNode;\n /** A complete notice body that replaces the title, description and progress. */\n content?: ReactNode;\n /** Collapsible detail below the header. */\n children?: ReactNode;\n}\n\nexport interface NotificationStackProps {\n /** Oldest first; the last entry renders nearest the corner. */\n notifications: Array<NotificationStackItem>;\n /** Fired by the close button and by the auto-close timer. */\n onClose?: (key: Key) => void;\n /** Cap on simultaneously visible notices; the newest win. Unlimited when unset. */\n maxVisible?: number;\n /** Extra class names on the stack. */\n className?: string;\n \"data-testid\"?: string;\n}\n\nfunction NotificationStackItemView({\n item,\n isExiting,\n onClose,\n}: {\n item: NotificationStackItem;\n isExiting: boolean;\n onClose?: (key: Key) => void;\n}) {\n const t = useUicTranslator();\n const { key, duration } = item;\n\n // The timer must not restart when the parent re-creates `onClose`, but\n // firing must still see the latest one.\n const fireClose = useEffectEvent(() => onClose?.(key));\n\n // A notice that closes under the pointer the reader moved there to read it\n // is lost; focus counts too, so keyboard users get the same reprieve.\n // Each holds the countdown on its own: leaving one must not resume it while\n // the other still holds.\n const [isHovered, setIsHovered] = useState(false);\n const [isFocused, setIsFocused] = useState(false);\n const isPaused = isHovered || isFocused;\n\n // Derived, not a default: a task is updated in place under the same key\n // (pending, then failed), so the item never remounts. `null` means the\n // reader has not touched the disclosure, so it follows the status.\n const isError = (item.status ?? \"info\") === \"error\";\n const [detailOpenByUser, setDetailOpenByUser] = useState<boolean | null>(null);\n const isDetailOpen = detailOpenByUser ?? isError;\n\n // `0` means \"stay open\", not \"close immediately\".\n const autoCloseMs =\n typeof duration === \"number\" && duration > 0 ? duration * 1000 : null;\n // What is left of the countdown, banked by the timer effect's cleanup so a\n // pause and resume continues rather than restarts.\n const remainingMsRef = useRef<number | null>(autoCloseMs);\n\n // A new duration is a new budget. Cleanups run before effects, so this\n // lands after the timer's cleanup banked the old value and before the timer\n // below reads it.\n useEffect(() => {\n remainingMsRef.current = autoCloseMs;\n }, [autoCloseMs]);\n\n useEffect(() => {\n if (autoCloseMs === null || isExiting || isPaused) return;\n const budget = remainingMsRef.current ?? autoCloseMs;\n const startedAt = Date.now();\n const timer = window.setTimeout(() => fireClose(), budget);\n return () => {\n window.clearTimeout(timer);\n remainingMsRef.current = Math.max(0, budget - (Date.now() - startedAt));\n };\n // An effect event is not a dependency; this plugin version predates it.\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [autoCloseMs, isExiting, isPaused]);\n\n const hasProgress =\n item.percent !== undefined || item.isProgressIndeterminate === true;\n const hasActions = !!(item.onCancel || item.onRetry || item.onAction);\n const hasOwnContent = item.content != null;\n\n // `wrap` lets the buttons stack inside the end area of a narrow notice\n // instead of pushing the header wider.\n const actions = (\n <HStack gap={2} align=\"center\" justify=\"end\" wrap=\"wrap\">\n {item.onCancel ? (\n <Button\n size=\"sm\"\n variant=\"ghost\"\n label={item.cancelText ?? t(\"uic.common.cancel\")}\n onClick={item.onCancel}\n />\n ) : null}\n {item.onRetry ? (\n <Button\n size=\"sm\"\n variant=\"secondary\"\n label={item.retryText ?? t(\"uic.common.retry\")}\n onClick={item.onRetry}\n />\n ) : null}\n {item.onAction && item.actionText ? (\n <Button\n size=\"sm\"\n variant=\"ghost\"\n label={item.actionText}\n onClick={item.onAction}\n />\n ) : null}\n </HStack>\n );\n\n return (\n <div\n className=\"uic-notification-stack__item\"\n data-exiting={isExiting ? \"true\" : \"false\"}\n data-notification-key={String(key)}\n data-status={item.status ?? \"info\"}\n data-paused={isPaused ? \"true\" : \"false\"}\n onMouseEnter={() => setIsHovered(true)}\n onMouseLeave={() => setIsHovered(false)}\n // React's onFocus/onBlur are the delegated focusin/focusout pair, so\n // focus anywhere inside the notice counts.\n onFocus={() => setIsFocused(true)}\n onBlur={() => setIsFocused(false)}\n >\n <Banner\n status={item.status ?? \"info\"}\n title={\n hasOwnContent ? (\n item.content\n ) : (\n <span data-testid=\"notification-title\">{item.title}</span>\n )\n }\n icon={item.icon}\n // Banner is normally in flow; a floating notice needs a shadow.\n elevation=\"high\"\n isDismissable={item.isClosable ?? true}\n onDismiss={() => onClose?.(key)}\n endContent={hasActions ? actions : undefined}\n collapsible={{\n isOpen: isDetailOpen,\n onOpenChange: setDetailOpenByUser,\n }}\n description={\n hasOwnContent ? undefined : item.description || hasProgress ? (\n <VStack gap={2} align=\"stretch\">\n {/* The text scrolls; a progress bar outside it stays pinned. */}\n {item.description ? (\n <div className=\"uic-notification-stack__body\">\n {typeof item.description === \"string\" ? (\n <Text type=\"supporting\">\n <span data-testid=\"notification-description\">\n {item.description}\n </span>\n </Text>\n ) : (\n item.description\n )}\n </div>\n ) : null}\n {hasProgress ? (\n <ProgressBar\n value={item.percent ?? 0}\n max={100}\n // Hidden: the Banner title already names the task on screen.\n label={\n item.progressLabel ??\n (typeof item.title === \"string\"\n ? item.title\n : t(\"uic.NotificationStack.progress\"))\n }\n isLabelHidden\n hasValueLabel={!item.isProgressIndeterminate}\n isIndeterminate={item.isProgressIndeterminate}\n />\n ) : null}\n </VStack>\n ) : undefined\n }\n >\n {/* A bare string would take Banner's base size and tower over the\n description, so it gets the description's treatment. */}\n {item.children ? (\n <div className=\"uic-notification-stack__body\">\n {typeof item.children === \"string\" ? (\n <Text type=\"supporting\">{item.children}</Text>\n ) : (\n item.children\n )}\n </div>\n ) : null}\n </Banner>\n </div>\n );\n}\n\nexport function NotificationStack({\n notifications,\n onClose,\n maxVisible,\n className,\n \"data-testid\": testId,\n}: NotificationStackProps) {\n // Notices that left `notifications` but are still playing their exit.\n const [exiting, setExiting] = useState<Array<NotificationStackItem>>([]);\n const previousVisibleRef = useRef<Array<NotificationStackItem>>([]);\n // One prune timer per exiting key. They outlive the effect that starts\n // them: any re-render (a new array, the exit itself) re-runs that effect,\n // and cancelling there would leave the notice mounted for good.\n const exitTimersRef = useRef(new Map<Key, number>());\n const stackRef = useRef<HTMLDivElement>(null);\n\n const visible = useMemo(\n () => (maxVisible ? notifications.slice(-maxVisible) : notifications),\n [notifications, maxVisible],\n );\n const newestKey = notifications.at(-1)?.key;\n\n // Once the stack is capped it scrolls, and the newest notice is at the\n // scrolled end. Keyed on the newest notice, not the array: a running task\n // rebuilds the array constantly and would yank a reader back down.\n useEffect(() => {\n const el = stackRef.current;\n if (el) el.scrollTop = el.scrollHeight;\n }, [newestKey]);\n\n useEffect(() => {\n const currentKeys = new Set(notifications.map((n) => n.key));\n // Only a notice that was on screen animates out; one closed while hidden\n // behind `maxVisible` would otherwise flash into the corner.\n const removed = previousVisibleRef.current.filter((n) => !currentKeys.has(n.key));\n previousVisibleRef.current = visible;\n if (removed.length === 0) return;\n const removedKeys = new Set(removed.map((n) => n.key));\n setExiting((prev) => [...prev.filter((n) => !removedKeys.has(n.key)), ...removed]);\n const timers = exitTimersRef.current;\n for (const { key } of removed) {\n window.clearTimeout(timers.get(key));\n timers.set(\n key,\n window.setTimeout(() => {\n timers.delete(key);\n setExiting((prev) => prev.filter((n) => n.key !== key));\n }, EXIT_ANIMATION_MS),\n );\n }\n }, [notifications, visible]);\n\n useEffect(() => {\n const timers = exitTimersRef.current;\n return () => {\n for (const timer of timers.values()) window.clearTimeout(timer);\n timers.clear();\n };\n }, []);\n\n const visibleKeys = new Set(visible.map((n) => n.key));\n const stillExiting = exiting.filter((n) => !visibleKeys.has(n.key));\n\n const isRendered = visible.length > 0 || stillExiting.length > 0;\n // The stack is marked to stay reachable over an open Modal; the modal stack\n // re-reads the mark whenever the stack appears or goes.\n useLayoutEffect(() => {\n refreshModalBackground();\n return () => queueMicrotask(refreshModalBackground);\n }, [isRendered]);\n\n if (!isRendered) return null;\n\n return (\n <div\n ref={stackRef}\n className={[\"uic-notification-stack\", className].filter(Boolean).join(\" \")}\n data-testid={testId}\n {...{ [MODAL_LIVE_ATTRIBUTE]: \"\" }}\n // Each Banner announces itself; the container stays out of the tree.\n role=\"presentation\"\n >\n {stillExiting.map((item) => (\n <NotificationStackItemView\n key={item.key}\n item={item}\n isExiting\n onClose={onClose}\n />\n ))}\n {visible.map((item) => (\n <NotificationStackItemView\n key={item.key}\n item={item}\n isExiting={false}\n onClose={onClose}\n />\n ))}\n </div>\n );\n}\n\nNotificationStack.displayName = \"NotificationStack\";\n"],"mappings":"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"}
1
+ {"version":3,"file":"NotificationStack2.js","names":[],"sources":["../../../src/components/NotificationStack/NotificationStack.tsx"],"sourcesContent":["/**\n * NotificationStack\n *\n * Floating notices stacked in the bottom-end corner, newest nearest the\n * corner. Each notice is an Astryx `Banner` that can carry what a toast\n * cannot: background-task progress (determinate or not), Cancel / Retry and a\n * navigation action, a collapsible detail, and a body of the caller's own.\n *\n * It is presentational. The caller owns the list: it adds notices, removes\n * them in `onClose`, and updates a notice in place under the same `key`.\n *\n * - `duration` closes a notice after that many seconds; the countdown pauses\n * while the notice is hovered or holds focus. `null` or `0` keeps it open.\n * - An error notice opens its detail (`children`) up front; the reader's own\n * toggle wins after that.\n * - `maxVisible` caps how many render; the newest win, and the rest render as\n * room frees up.\n * - Removed notices slide out before they unmount, unless the reader asked\n * for reduced motion.\n * - A long description scrolls inside its notice, so the dismiss button stays\n * on screen; the stack scrolls once it reaches the top inset.\n *\n * Layout hooks, set on the stack or an ancestor:\n * `--notification-stack-z` (stacking order, default 11000, one above\n * `Modal`'s default band), `--notification-stack-inset-top` (space kept\n * free above the stack, such as an app header, default 0) and\n * `--notification-stack-body-max-height` (one notice's scroll cap, default 30vh).\n *\n * e2e hooks: each notice carries `data-notification-key`, `data-status` and\n * `data-paused`; its title and description carry `data-testid`\n * `notification-title` and `notification-description`.\n *\n * @example\n * <NotificationStack\n * notifications={[{ key: \"upload\", title: \"Uploading\", percent: 40 }]}\n * onClose={(key) => remove(key)}\n * />\n */\nimport {\n useEffect,\n useEffectEvent,\n useLayoutEffect,\n useMemo,\n useRef,\n useState,\n type Key,\n type ReactNode,\n} from \"react\";\nimport { Banner, type BannerStatus } from \"@astryxdesign/core/Banner\";\nimport { Button } from \"@astryxdesign/core/Button\";\nimport { ProgressBar } from \"@astryxdesign/core/ProgressBar\";\nimport { HStack, VStack } from \"@astryxdesign/core/Stack\";\nimport { Text } from \"@astryxdesign/core/Text\";\n\nimport { useUicTranslator } from \"../../i18n/useUicTranslator\";\nimport { MODAL_LIVE_ATTRIBUTE, refreshModalBackground } from \"../Modal/modalStack\";\nimport \"./NotificationStack.css\";\n\n/** Matches the exit animation's budget in NotificationStack.css. */\nconst EXIT_ANIMATION_MS = 200;\n\nexport interface NotificationStackItem {\n key: Key;\n /** Headline. */\n title: ReactNode;\n description?: ReactNode;\n /** Drives the Banner's icon and colour. @default 'info' */\n status?: BannerStatus;\n /** Background-task progress, 0-100. Omit for a notice with no task. */\n percent?: number;\n /** A task that is running with no measurable progress yet. */\n isProgressIndeterminate?: boolean;\n /**\n * Accessible name of the progress bar.\n * @default the title when it is a string, else the catalog's uic.NotificationStack.progress\n */\n progressLabel?: string;\n /** Label of the navigation action (\"View folder\"). Shown with `onAction`. */\n actionText?: string;\n onAction?: () => void;\n /** @default the catalog's uic.common.retry (\"Retry\") */\n retryText?: string;\n onRetry?: () => void;\n /** @default the catalog's uic.common.cancel (\"Cancel\") */\n cancelText?: string;\n onCancel?: () => void;\n /**\n * Seconds until the notice closes itself; `null` or `0` keeps it open until\n * dismissed. The countdown pauses while the notice is hovered or focused.\n */\n duration?: number | null;\n /** @default true */\n isClosable?: boolean;\n /** Overrides the status icon. */\n icon?: ReactNode;\n /** A complete notice body that replaces the title, description and progress. */\n content?: ReactNode;\n /** Collapsible detail below the header. */\n children?: ReactNode;\n}\n\nexport interface NotificationStackProps {\n /** Oldest first; the last entry renders nearest the corner. */\n notifications: Array<NotificationStackItem>;\n /** Fired by the close button and by the auto-close timer. */\n onClose?: (key: Key) => void;\n /** Cap on simultaneously visible notices; the newest win. Unlimited when unset. */\n maxVisible?: number;\n /** Extra class names on the stack. */\n className?: string;\n \"data-testid\"?: string;\n}\n\nfunction NotificationStackItemView({\n item,\n isExiting,\n onClose,\n}: {\n item: NotificationStackItem;\n isExiting: boolean;\n onClose?: (key: Key) => void;\n}) {\n const t = useUicTranslator();\n const { key, duration } = item;\n\n // The timer must not restart when the parent re-creates `onClose`, but\n // firing must still see the latest one.\n const fireClose = useEffectEvent(() => onClose?.(key));\n\n // A notice that closes under the pointer the reader moved there to read it\n // is lost; focus counts too, so keyboard users get the same reprieve.\n // Each holds the countdown on its own: leaving one must not resume it while\n // the other still holds.\n const [isHovered, setIsHovered] = useState(false);\n const [isFocused, setIsFocused] = useState(false);\n const isPaused = isHovered || isFocused;\n\n // Derived, not a default: a task is updated in place under the same key\n // (pending, then failed), so the item never remounts. `null` means the\n // reader has not touched the disclosure, so it follows the status.\n const isError = (item.status ?? \"info\") === \"error\";\n const [detailOpenByUser, setDetailOpenByUser] = useState<boolean | null>(null);\n const isDetailOpen = detailOpenByUser ?? isError;\n\n // `0` means \"stay open\", not \"close immediately\".\n const autoCloseMs =\n typeof duration === \"number\" && duration > 0 ? duration * 1000 : null;\n // What is left of the countdown, banked by the timer effect's cleanup so a\n // pause and resume continues rather than restarts.\n const remainingMsRef = useRef<number | null>(autoCloseMs);\n\n // A new duration is a new budget. Cleanups run before effects, so this\n // lands after the timer's cleanup banked the old value and before the timer\n // below reads it.\n useEffect(() => {\n remainingMsRef.current = autoCloseMs;\n }, [autoCloseMs]);\n\n useEffect(() => {\n if (autoCloseMs === null || isExiting || isPaused) return;\n const budget = remainingMsRef.current ?? autoCloseMs;\n const startedAt = Date.now();\n const timer = window.setTimeout(() => fireClose(), budget);\n return () => {\n window.clearTimeout(timer);\n remainingMsRef.current = Math.max(0, budget - (Date.now() - startedAt));\n };\n // An effect event is not a dependency; this plugin version predates it.\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [autoCloseMs, isExiting, isPaused]);\n\n const hasProgress =\n item.percent !== undefined || item.isProgressIndeterminate === true;\n const hasActions = !!(item.onCancel || item.onRetry || item.onAction);\n const hasOwnContent = item.content != null;\n\n // `wrap` lets the buttons stack inside the end area of a narrow notice\n // instead of pushing the header wider.\n const actions = (\n <HStack gap={2} align=\"center\" justify=\"end\" wrap=\"wrap\">\n {item.onCancel ? (\n <Button\n size=\"sm\"\n variant=\"ghost\"\n label={item.cancelText ?? t(\"uic.common.cancel\")}\n onClick={item.onCancel}\n />\n ) : null}\n {item.onRetry ? (\n <Button\n size=\"sm\"\n variant=\"secondary\"\n label={item.retryText ?? t(\"uic.common.retry\")}\n onClick={item.onRetry}\n />\n ) : null}\n {item.onAction && item.actionText ? (\n <Button\n size=\"sm\"\n variant=\"ghost\"\n label={item.actionText}\n onClick={item.onAction}\n />\n ) : null}\n </HStack>\n );\n\n return (\n <div\n className=\"uic-notification-stack__item\"\n data-exiting={isExiting ? \"true\" : \"false\"}\n data-notification-key={String(key)}\n data-status={item.status ?? \"info\"}\n data-paused={isPaused ? \"true\" : \"false\"}\n onMouseEnter={() => setIsHovered(true)}\n onMouseLeave={() => setIsHovered(false)}\n // React's onFocus/onBlur are the delegated focusin/focusout pair, so\n // focus anywhere inside the notice counts.\n onFocus={() => setIsFocused(true)}\n onBlur={() => setIsFocused(false)}\n >\n <Banner\n status={item.status ?? \"info\"}\n title={\n hasOwnContent ? (\n item.content\n ) : (\n <span data-testid=\"notification-title\">{item.title}</span>\n )\n }\n icon={item.icon}\n // Astryx 0.7 no longer derives a live role from `status`; a notice\n // arrives asynchronously, so it keeps the one it had.\n role={item.status === \"error\" || item.status === \"warning\" ? \"alert\" : \"status\"}\n // Banner is normally in flow; a floating notice needs a shadow.\n elevation=\"high\"\n isDismissable={item.isClosable ?? true}\n onDismiss={() => onClose?.(key)}\n endContent={hasActions ? actions : undefined}\n collapsible={{\n isOpen: isDetailOpen,\n onOpenChange: setDetailOpenByUser,\n }}\n description={\n hasOwnContent ? undefined : item.description || hasProgress ? (\n <VStack gap={2} align=\"stretch\">\n {/* The text scrolls; a progress bar outside it stays pinned. */}\n {item.description ? (\n <div className=\"uic-notification-stack__body\">\n {typeof item.description === \"string\" ? (\n <Text type=\"supporting\">\n <span data-testid=\"notification-description\">\n {item.description}\n </span>\n </Text>\n ) : (\n item.description\n )}\n </div>\n ) : null}\n {hasProgress ? (\n <ProgressBar\n value={item.percent ?? 0}\n max={100}\n // Hidden: the Banner title already names the task on screen.\n label={\n item.progressLabel ??\n (typeof item.title === \"string\"\n ? item.title\n : t(\"uic.NotificationStack.progress\"))\n }\n isLabelHidden\n hasValueLabel={!item.isProgressIndeterminate}\n isIndeterminate={item.isProgressIndeterminate}\n />\n ) : null}\n </VStack>\n ) : undefined\n }\n >\n {/* A bare string would take Banner's base size and tower over the\n description, so it gets the description's treatment. */}\n {item.children ? (\n <div className=\"uic-notification-stack__body\">\n {typeof item.children === \"string\" ? (\n <Text type=\"supporting\">{item.children}</Text>\n ) : (\n item.children\n )}\n </div>\n ) : null}\n </Banner>\n </div>\n );\n}\n\nexport function NotificationStack({\n notifications,\n onClose,\n maxVisible,\n className,\n \"data-testid\": testId,\n}: NotificationStackProps) {\n // Notices that left `notifications` but are still playing their exit.\n const [exiting, setExiting] = useState<Array<NotificationStackItem>>([]);\n const previousVisibleRef = useRef<Array<NotificationStackItem>>([]);\n // One prune timer per exiting key. They outlive the effect that starts\n // them: any re-render (a new array, the exit itself) re-runs that effect,\n // and cancelling there would leave the notice mounted for good.\n const exitTimersRef = useRef(new Map<Key, number>());\n const stackRef = useRef<HTMLDivElement>(null);\n\n const visible = useMemo(\n () => (maxVisible ? notifications.slice(-maxVisible) : notifications),\n [notifications, maxVisible],\n );\n const newestKey = notifications.at(-1)?.key;\n\n // Once the stack is capped it scrolls, and the newest notice is at the\n // scrolled end. Keyed on the newest notice, not the array: a running task\n // rebuilds the array constantly and would yank a reader back down.\n useEffect(() => {\n const el = stackRef.current;\n if (el) el.scrollTop = el.scrollHeight;\n }, [newestKey]);\n\n useEffect(() => {\n const currentKeys = new Set(notifications.map((n) => n.key));\n // Only a notice that was on screen animates out; one closed while hidden\n // behind `maxVisible` would otherwise flash into the corner.\n const removed = previousVisibleRef.current.filter((n) => !currentKeys.has(n.key));\n previousVisibleRef.current = visible;\n if (removed.length === 0) return;\n const removedKeys = new Set(removed.map((n) => n.key));\n setExiting((prev) => [...prev.filter((n) => !removedKeys.has(n.key)), ...removed]);\n const timers = exitTimersRef.current;\n for (const { key } of removed) {\n window.clearTimeout(timers.get(key));\n timers.set(\n key,\n window.setTimeout(() => {\n timers.delete(key);\n setExiting((prev) => prev.filter((n) => n.key !== key));\n }, EXIT_ANIMATION_MS),\n );\n }\n }, [notifications, visible]);\n\n useEffect(() => {\n const timers = exitTimersRef.current;\n return () => {\n for (const timer of timers.values()) window.clearTimeout(timer);\n timers.clear();\n };\n }, []);\n\n const visibleKeys = new Set(visible.map((n) => n.key));\n const stillExiting = exiting.filter((n) => !visibleKeys.has(n.key));\n\n const isRendered = visible.length > 0 || stillExiting.length > 0;\n // The stack is marked to stay reachable over an open Modal; the modal stack\n // re-reads the mark whenever the stack appears or goes.\n useLayoutEffect(() => {\n refreshModalBackground();\n return () => queueMicrotask(refreshModalBackground);\n }, [isRendered]);\n\n if (!isRendered) return null;\n\n return (\n <div\n ref={stackRef}\n className={[\"uic-notification-stack\", className].filter(Boolean).join(\" \")}\n data-testid={testId}\n {...{ [MODAL_LIVE_ATTRIBUTE]: \"\" }}\n // Each Banner announces itself (its explicit role); the container stays out of the tree.\n role=\"presentation\"\n >\n {stillExiting.map((item) => (\n <NotificationStackItemView\n key={item.key}\n item={item}\n isExiting\n onClose={onClose}\n />\n ))}\n {visible.map((item) => (\n <NotificationStackItemView\n key={item.key}\n item={item}\n isExiting={false}\n onClose={onClose}\n />\n ))}\n </div>\n );\n}\n\nNotificationStack.displayName = \"NotificationStack\";\n"],"mappings":"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"}
@@ -19,7 +19,7 @@ export interface PagedSelectorSelectedItem {
19
19
  value: string;
20
20
  label: string;
21
21
  }
22
- type Passthrough = Pick<ComplexSelectorProps<string[]>, "label" | "isLabelHidden" | "description" | "isOptional" | "isRequired" | "isDisabled" | "isLoading" | "status" | "statusVariant" | "labelTooltip" | "size" | "variant" | "startIcon" | "width" | "placement" | "alignment" | "onOpenChange" | "xstyle" | "className" | "style" | "data-testid">;
22
+ type Passthrough = Pick<ComplexSelectorProps<string[]>, "label" | "isLabelHidden" | "description" | "isOptional" | "isRequired" | "isDisabled" | "isLoading" | "status" | "statusVariant" | "labelTooltip" | "size" | "variant" | "startIcon" | "width" | "placement" | "alignment" | "onOpenChange" | "containerXstyle" | "containerClassName" | "containerStyle" | "controlXstyle" | "controlClassName" | "controlStyle" | "data-testid">;
23
23
  interface PagedSelectorBaseProps extends Passthrough {
24
24
  /** The loaded options, every page so far, in order. */
25
25
  options: readonly PagedSelectorOption[];
@@ -1 +1 @@
1
- {"version":3,"file":"PagedSelector2.js","names":[],"sources":["../../../src/components/PagedSelector/PagedSelector.tsx"],"sourcesContent":["/**\n * PagedSelector\n *\n * A searchable selector, single or multiple, over a list that loads a page at\n * a time: scrolling the panel near its end calls `onEndReached`, which is\n * where a consumer loads the next page (a Relay `loadNext`, a cursor fetch).\n * Search is the consumer's too: `onSearchChange` reports every keystroke.\n *\n * Astryx's `Selector` / `MultiSelector` mount every option and have no\n * scroll callback; `Typeahead` replaces its list on each query. This is\n * built on `ComplexSelector` (ui-common's copy), which owns the field,\n * trigger, popover and focus return and hands the panel body back, so the\n * body can own the scroll container. The body is drawn the way `Selector`'s\n * is: Astryx's panel search row, then option rows with the theme's check.\n *\n * - `value` holds option values: a string (or null) alone, a string array\n * with `isMultiple`. `onChange` also gets each chosen value's label, so a\n * consumer can keep labels for values that are not on the loaded page.\n * - A selected value missing from `options` (on another page, or filtered out\n * by the query) is named from `labels`, then from any page it was seen on,\n * then shown as the value itself.\n * - Keyboard: ArrowUp/Down, Home/End move a highlight that skips disabled\n * rows; Enter commits it, and Space too when there is no search box. The\n * panel is `ComplexSelector`'s dialog holding a listbox, not an ARIA\n * combobox.\n * - Options render one DOM row each; paging is what keeps that bounded.\n *\n * @example\n * <PagedSelector\n * label=\"Owner\"\n * value={ownerId}\n * onChange={(id) => setOwnerId(id)}\n * options={users.map((u) => ({ value: u.id, label: u.email }))}\n * onSearchChange={setQuery}\n * onEndReached={() => hasNext && loadNext(10)}\n * isLoadingMore={isLoadingNext}\n * totalCount={count}\n * />\n */\nimport { useId, useLayoutEffect, useRef, useState, type ReactNode } from \"react\";\nimport {\n ComplexSelector,\n type ComplexSelectorProps,\n} from \"../../forks/ComplexSelector\";\nimport { compiledStyles } from \"../../forks/compiled\";\nimport { Divider } from \"@astryxdesign/core/Divider\";\nimport { useIndicator } from \"@astryxdesign/core/Indicator\";\nimport { SelectorOption } from \"@astryxdesign/core/Selector\";\nimport { Spinner } from \"@astryxdesign/core/Spinner\";\nimport { HStack } from \"@astryxdesign/core/Stack\";\nimport { Text } from \"@astryxdesign/core/Text\";\nimport { Token } from \"@astryxdesign/core/Token\";\nimport { VisuallyHidden } from \"@astryxdesign/core/VisuallyHidden\";\nimport { themeProps } from \"@astryxdesign/core/utils\";\n\nimport { useUicTranslator } from \"../../i18n/useUicTranslator\";\nimport { PanelSearchInput } from \"./PanelSearchInput\";\nimport \"./PagedSelector.css\";\n\nexport interface PagedSelectorOption {\n value: string;\n /** The option's name: the trigger text and the row's accessible name. */\n label: string;\n /** Drawn in the row in place of `label`, for a row richer than a string. */\n labelContent?: ReactNode;\n /** Leading visual (avatar, glyph). */\n icon?: ReactNode;\n /** Secondary line under the label. */\n description?: ReactNode;\n /** Trailing content (tokens, meta). */\n endContent?: ReactNode;\n isDisabled?: boolean;\n}\n\n/** A chosen value with its label, as `onChange` reports it. */\nexport interface PagedSelectorSelectedItem {\n value: string;\n label: string;\n}\n\ntype Passthrough = Pick<\n ComplexSelectorProps<string[]>,\n | \"label\"\n | \"isLabelHidden\"\n | \"description\"\n | \"isOptional\"\n | \"isRequired\"\n | \"isDisabled\"\n | \"isLoading\"\n | \"status\"\n | \"statusVariant\"\n | \"labelTooltip\"\n | \"size\"\n | \"variant\"\n | \"startIcon\"\n | \"width\"\n | \"placement\"\n | \"alignment\"\n | \"onOpenChange\"\n | \"xstyle\"\n | \"className\"\n | \"style\"\n | \"data-testid\"\n>;\n\ninterface PagedSelectorBaseProps extends Passthrough {\n /** The loaded options, every page so far, in order. */\n options: readonly PagedSelectorOption[];\n /** Labels for selected values that may not be among `options`. */\n labels?: Readonly<Record<string, string>>;\n /**\n * Placeholder while nothing is selected.\n * @default the catalog's uic.PagedSelector.placeholder (\"Select {label}\")\n */\n placeholder?: string;\n /** Show the search box at the top of the panel. @default true */\n hasSearch?: boolean;\n /** Controlled search text; uncontrolled without it. */\n searchValue?: string;\n /** Called on every keystroke in the search box; debounce it yourself. */\n onSearchChange?: (searchValue: string) => void;\n /** @default the catalog's uic.PagedSelector.searchPlaceholder (\"Search\") */\n searchPlaceholder?: string;\n /** @default the catalog's uic.PagedSelector.searchOptions (\"Search options\") */\n searchLabel?: string;\n /** @default the catalog's uic.PagedSelector.clearSearch (\"Clear search options\") */\n clearSearchLabel?: string;\n /**\n * Called once each time the list is scrolled to within\n * `endReachedThreshold` px of its end. Load the next page here.\n */\n onEndReached?: () => void;\n /** Distance from the end, in px, that counts as reaching it. @default 30 */\n endReachedThreshold?: number;\n /** Called when the list arrives at, or leaves, its end. */\n onAtEndChange?: (isAtEnd: boolean) => void;\n /** A spinner in the foot while the next page loads. */\n isLoadingMore?: boolean;\n /** Options in all pages; shows the \"Total N items\" foot when positive. */\n totalCount?: number;\n /** @default the catalog's uic.PagedSelector.totalItems (\"Total {total} items\") */\n formatTotalCount?: (total: number) => string;\n /**\n * Replaces the empty list's content, the loading row included.\n * @default the catalog's uic.PagedSelector.loading while `isLoading`, else\n * uic.PagedSelector.noResults (\"No results\")\n */\n emptyText?: ReactNode;\n /** Above the option list, under the search box. */\n header?: ReactNode;\n /** Below the option list, in place of the total count. A function gets `close`. */\n footer?: ReactNode | ((close: () => void) => ReactNode);\n /** Height the option list scrolls within, in px. @default 260 */\n listMaxHeight?: number;\n /**\n * How a multiple selection shows in the trigger: `labels` (\"A, B, C, +2\",\n * as `MultiSelector`) or `badges` (tokens). @default \"labels\"\n */\n triggerDisplay?: \"labels\" | \"badges\";\n /** Selected items the trigger names before \"+N\". @default 3 */\n maxTriggerItems?: number;\n /** How a selected row is marked: the theme's check at its end, or a checkbox at its start. @default \"check\" */\n selectionIndicator?: \"check\" | \"checkbox\";\n /** A clear button in the trigger while something is selected. */\n hasClear?: boolean;\n /** Runs on clear. @default onChange(null) alone, onChange([]) with isMultiple */\n onClear?: () => void;\n}\n\nexport interface PagedSelectorSingleProps extends PagedSelectorBaseProps {\n isMultiple?: false;\n value?: string | null;\n onChange?: (value: string | null, item: PagedSelectorSelectedItem | null) => void;\n}\n\nexport interface PagedSelectorMultipleProps extends PagedSelectorBaseProps {\n isMultiple: true;\n value?: readonly string[];\n onChange?: (value: string[], items: PagedSelectorSelectedItem[]) => void;\n}\n\nexport type PagedSelectorProps = PagedSelectorSingleProps | PagedSelectorMultipleProps;\n\n/**\n * `ComplexSelector` insets its panel body by `--spacing-3`; this body brings\n * Selector's own gutters, so the inset goes. Astryx's compiled `padding: 0`\n * class (kmVPX3 is StyleX's key for `padding`), so no StyleX compiler is\n * needed; the test suite fails if astryx.css stops defining it.\n */\nexport const CONTENT_RESET = { padding: \"x1717udv\" } as const;\nconst contentStyles = compiledStyles({\n reset: { kmVPX3: CONTENT_RESET.padding, $$css: true },\n});\n\nexport function PagedSelector(props: PagedSelectorProps) {\n const {\n label,\n options,\n labels,\n placeholder,\n hasSearch = true,\n searchValue,\n onSearchChange,\n searchPlaceholder,\n searchLabel,\n clearSearchLabel,\n onEndReached,\n endReachedThreshold = 30,\n onAtEndChange,\n isLoadingMore,\n totalCount,\n formatTotalCount,\n emptyText,\n header,\n footer,\n listMaxHeight = 260,\n triggerDisplay = \"labels\",\n maxTriggerItems = 3,\n selectionIndicator = \"check\",\n hasClear,\n onClear,\n onOpenChange,\n isLoading,\n size,\n isMultiple,\n value: _value,\n onChange: _onChange,\n ...rest\n } = props;\n const t = useUicTranslator();\n const CheckMark = useIndicator(\"check\");\n const CheckboxMark = useIndicator(\"checkbox\");\n const listboxId = useId();\n const optionIdPrefix = useId();\n\n const selectedValues: string[] = props.isMultiple\n ? [...(props.value ?? [])]\n : props.value != null\n ? [props.value]\n : [];\n\n // Labels of every option seen, so a value picked from a page the list has\n // since dropped (a new query) keeps its name.\n const seenLabels = useRef(new Map<string, string>());\n for (const option of options) seenLabels.current.set(option.value, option.label);\n const labelOf = (value: string) =>\n labels?.[value] ??\n options.find((option) => option.value === value)?.label ??\n seenLabels.current.get(value) ??\n value;\n\n const emit = (values: string[]) => {\n const items = values.map((value) => ({ value, label: labelOf(value) }));\n if (props.isMultiple) props.onChange?.(values, items);\n else props.onChange?.(values[0] ?? null, items[0] ?? null);\n };\n\n // -1: an opened Astryx Selector highlights nothing until the pointer or an\n // arrow key picks a row.\n const [highlightedIndex, setHighlightedIndex] = useState(-1);\n const [internalSearch, setInternalSearch] = useState(\"\");\n const search = searchValue ?? internalSearch;\n const isAtEnd = useRef(false);\n\n const optionIdOf = (index: number) => `${optionIdPrefix}-option-${index}`;\n const clampedIndex =\n options.length === 0 || highlightedIndex < 0\n ? -1\n : Math.min(highlightedIndex, options.length - 1);\n // A row that went disabled under a page append does not keep the highlight.\n const activeIndex =\n clampedIndex >= 0 && options[clampedIndex]?.isDisabled ? -1 : clampedIndex;\n\n const nextEnabledIndex = (from: number, step: 1 | -1) => {\n for (let i = from; i >= 0 && i < options.length; i += step) {\n if (!options[i]?.isDisabled) return i;\n }\n return -1;\n };\n\n useLayoutEffect(() => {\n if (activeIndex < 0) return;\n document\n .getElementById(optionIdOf(activeIndex))\n ?.scrollIntoView({ block: \"nearest\" });\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [activeIndex]);\n\n const handleScroll = (event: React.UIEvent<HTMLDivElement>) => {\n if (!onEndReached && !onAtEndChange) return;\n const el = event.currentTarget;\n const isAtEndNow =\n el.scrollHeight - el.scrollTop - el.clientHeight <= endReachedThreshold;\n if (isAtEndNow === isAtEnd.current) return;\n isAtEnd.current = isAtEndNow;\n onAtEndChange?.(isAtEndNow);\n if (isAtEndNow) onEndReached?.();\n };\n\n const isSelected = (value: string) => selectedValues.includes(value);\n\n const commit = (option: PagedSelectorOption, close: () => void) => {\n if (!isMultiple) {\n emit([option.value]);\n close();\n return;\n }\n emit(\n isSelected(option.value)\n ? selectedValues.filter((value) => value !== option.value)\n : [...selectedValues, option.value],\n );\n };\n\n // Shared by the search box and, with no search box, the listbox itself.\n // Space commits only where it is not typing into the search box.\n const handleNavKeyDown = (\n event: React.KeyboardEvent<HTMLElement>,\n close: () => void,\n hasSpaceCommit: boolean,\n ) => {\n if (options.length === 0) return;\n const moveTo = (index: number) => {\n if (index >= 0) setHighlightedIndex(index);\n };\n switch (event.key) {\n case \"ArrowDown\":\n event.preventDefault();\n moveTo(nextEnabledIndex(activeIndex < 0 ? 0 : activeIndex + 1, 1));\n break;\n case \"ArrowUp\":\n event.preventDefault();\n moveTo(\n activeIndex < 0\n ? nextEnabledIndex(0, 1)\n : nextEnabledIndex(activeIndex - 1, -1),\n );\n break;\n case \"Home\":\n event.preventDefault();\n moveTo(nextEnabledIndex(0, 1));\n break;\n case \"End\":\n event.preventDefault();\n moveTo(nextEnabledIndex(options.length - 1, -1));\n break;\n case \" \":\n case \"Enter\": {\n if (event.key === \" \" && !hasSpaceCommit) break;\n const option = options[activeIndex];\n if (!option || option.isDisabled) break;\n event.preventDefault();\n commit(option, close);\n break;\n }\n default:\n break;\n }\n };\n\n const triggerLabel = (() => {\n if (selectedValues.length === 0) return undefined;\n if (!isMultiple) return labelOf(selectedValues[0]!);\n const shown = selectedValues.slice(0, maxTriggerItems);\n const remaining = selectedValues.length - shown.length;\n if (triggerDisplay === \"badges\") {\n // Display-only: the trigger is ComplexSelector's own <button>, so a\n // removable token would nest a button in a button.\n return (\n <HStack gap={0.5} vAlign=\"center\" wrap=\"wrap\">\n {shown.map((value) => (\n <Token key={value} label={labelOf(value)} size=\"sm\" />\n ))}\n {remaining > 0 ? <Text color=\"secondary\">{`+${remaining}`}</Text> : null}\n </HStack>\n );\n }\n const joined = shown.map(labelOf).join(\", \");\n return remaining > 0 ? `${joined}, +${remaining}` : joined;\n })();\n\n const totalText = (total: number) =>\n formatTotalCount?.(total) ?? t(\"uic.PagedSelector.totalItems\", { total });\n const loadingText = t(\"uic.PagedSelector.loading\");\n\n return (\n <ComplexSelector<string[]>\n {...rest}\n label={label}\n value={selectedValues}\n onChange={emit}\n triggerLabel={triggerLabel}\n placeholder={placeholder ?? t(\"uic.PagedSelector.placeholder\", { label })}\n isLoading={isLoading}\n size={size}\n hasClear={hasClear}\n onClear={onClear ?? (() => emit([]))}\n onOpenChange={(isOpen) => {\n // Astryx drops its highlight when the panel closes, so the next open\n // starts clean.\n if (!isOpen) setHighlightedIndex(-1);\n onOpenChange?.(isOpen);\n }}\n contentXstyle={contentStyles.reset}\n >\n {(_current, _commit, close) => (\n <div>\n {hasSearch ? (\n <>\n <PanelSearchInput\n label={searchLabel ?? t(\"uic.PagedSelector.searchOptions\")}\n clearLabel={clearSearchLabel ?? t(\"uic.PagedSelector.clearSearch\")}\n placeholder={\n searchPlaceholder ?? t(\"uic.PagedSelector.searchPlaceholder\")\n }\n value={search}\n onValueChange={(next) => {\n setInternalSearch(next);\n // Seeds the first selectable row, so type-then-Enter works.\n setHighlightedIndex(nextEnabledIndex(0, 1));\n onSearchChange?.(next);\n }}\n onKeyDown={(event) => handleNavKeyDown(event, close, false)}\n aria-controls={listboxId}\n aria-activedescendant={\n activeIndex >= 0 ? optionIdOf(activeIndex) : undefined\n }\n />\n <Divider />\n </>\n ) : null}\n {header}\n <div\n id={listboxId}\n role=\"listbox\"\n aria-label={label}\n aria-multiselectable={isMultiple || undefined}\n // With no search box nothing else in the panel takes focus, so the\n // popover's autofocus lands here and the arrows work.\n tabIndex={hasSearch ? undefined : 0}\n aria-activedescendant={\n !hasSearch && activeIndex >= 0 ? optionIdOf(activeIndex) : undefined\n }\n onKeyDown={\n hasSearch ? undefined : (event) => handleNavKeyDown(event, close, true)\n }\n onScroll={handleScroll}\n className=\"uic-paged-selector__listbox\"\n style={{ maxHeight: listMaxHeight }}\n >\n {options.length === 0 ? (\n <div className=\"uic-paged-selector__empty\">\n {emptyText !== undefined ? (\n emptyText\n ) : isLoading ? (\n <HStack gap={1} vAlign=\"center\" hAlign=\"center\">\n <Spinner size=\"sm\" />\n <Text color=\"secondary\">{loadingText}</Text>\n </HStack>\n ) : (\n <Text color=\"secondary\">{t(\"uic.PagedSelector.noResults\")}</Text>\n )}\n </div>\n ) : (\n options.map((option, index) => {\n const selected = isSelected(option.value);\n const isDisabled = option.isDisabled ?? false;\n return (\n <div\n key={option.value}\n id={optionIdOf(index)}\n role=\"option\"\n aria-selected={selected}\n aria-disabled={isDisabled || undefined}\n className=\"uic-paged-selector__option\"\n data-size={size === \"sm\" ? \"sm\" : undefined}\n data-selected={selected ? \"true\" : undefined}\n data-highlighted={index === activeIndex ? \"true\" : undefined}\n data-disabled={isDisabled ? \"true\" : undefined}\n onClick={() => {\n if (isDisabled) return;\n setHighlightedIndex(index);\n commit(option, close);\n }}\n onMouseEnter={() => {\n if (!isDisabled) setHighlightedIndex(index);\n }}\n >\n {selectionIndicator === \"checkbox\" && (\n <span className=\"uic-paged-selector__option-mark\">\n <CheckboxMark\n state={selected ? \"checked\" : \"unchecked\"}\n size=\"sm\"\n isDisabled={isDisabled}\n />\n </span>\n )}\n <span className=\"uic-paged-selector__option-content\">\n <SelectorOption\n icon={option.icon}\n label={option.labelContent ?? option.label}\n description={option.description}\n endContent={option.endContent}\n />\n </span>\n {/* Rendered with the state passed down: a theme that swaps\n the check for a radio draws its empty circle too. */}\n {selectionIndicator === \"check\" && (\n <span className=\"uic-paged-selector__option-mark\">\n <CheckMark\n state={selected ? \"checked\" : \"unchecked\"}\n size=\"sm\"\n isDisabled={isDisabled}\n {...themeProps(\"selector-check\")}\n />\n </span>\n )}\n </div>\n );\n })\n )}\n </div>\n {(typeof footer === \"function\" ? footer(close) : footer) ??\n (typeof totalCount === \"number\" && totalCount > 0 ? (\n <HStack\n gap={1}\n vAlign=\"center\"\n hAlign=\"end\"\n className=\"uic-paged-selector__foot\"\n >\n {isLoadingMore ? <Spinner size=\"sm\" /> : null}\n <Text color=\"secondary\" size=\"sm\">\n {totalText(totalCount)}\n </Text>\n </HStack>\n ) : null)}\n <VisuallyHidden as=\"div\" aria-live=\"polite\">\n {options.length === 0 && isLoading\n ? loadingText\n : totalText(options.length)}\n </VisuallyHidden>\n </div>\n )}\n </ComplexSelector>\n );\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"PagedSelector2.js","names":[],"sources":["../../../src/components/PagedSelector/PagedSelector.tsx"],"sourcesContent":["/**\n * PagedSelector\n *\n * A searchable selector, single or multiple, over a list that loads a page at\n * a time: scrolling the panel near its end calls `onEndReached`, which is\n * where a consumer loads the next page (a Relay `loadNext`, a cursor fetch).\n * Search is the consumer's too: `onSearchChange` reports every keystroke.\n *\n * Astryx's `Selector` / `MultiSelector` mount every option and have no\n * scroll callback; `Typeahead` replaces its list on each query. This is\n * built on `ComplexSelector` (ui-common's copy), which owns the field,\n * trigger, popover and focus return and hands the panel body back, so the\n * body can own the scroll container. The body is drawn the way `Selector`'s\n * is: Astryx's panel search row, then option rows with the theme's check.\n *\n * - `value` holds option values: a string (or null) alone, a string array\n * with `isMultiple`. `onChange` also gets each chosen value's label, so a\n * consumer can keep labels for values that are not on the loaded page.\n * - A selected value missing from `options` (on another page, or filtered out\n * by the query) is named from `labels`, then from any page it was seen on,\n * then shown as the value itself.\n * - Keyboard: ArrowUp/Down, Home/End move a highlight that skips disabled\n * rows; Enter commits it, and Space too when there is no search box. The\n * panel is `ComplexSelector`'s dialog holding a listbox, not an ARIA\n * combobox.\n * - Options render one DOM row each; paging is what keeps that bounded.\n *\n * @example\n * <PagedSelector\n * label=\"Owner\"\n * value={ownerId}\n * onChange={(id) => setOwnerId(id)}\n * options={users.map((u) => ({ value: u.id, label: u.email }))}\n * onSearchChange={setQuery}\n * onEndReached={() => hasNext && loadNext(10)}\n * isLoadingMore={isLoadingNext}\n * totalCount={count}\n * />\n */\nimport { useId, useLayoutEffect, useRef, useState, type ReactNode } from \"react\";\nimport {\n ComplexSelector,\n type ComplexSelectorProps,\n} from \"../../forks/ComplexSelector\";\nimport { compiledStyles } from \"../../forks/compiled\";\nimport { Divider } from \"@astryxdesign/core/Divider\";\nimport { useIndicator } from \"@astryxdesign/core/Indicator\";\nimport { SelectorOption } from \"@astryxdesign/core/Selector\";\nimport { Spinner } from \"@astryxdesign/core/Spinner\";\nimport { HStack } from \"@astryxdesign/core/Stack\";\nimport { Text } from \"@astryxdesign/core/Text\";\nimport { Token } from \"@astryxdesign/core/Token\";\nimport { VisuallyHidden } from \"@astryxdesign/core/VisuallyHidden\";\nimport { themeProps } from \"@astryxdesign/core/utils\";\n\nimport { useUicTranslator } from \"../../i18n/useUicTranslator\";\nimport { PanelSearchInput } from \"./PanelSearchInput\";\nimport \"./PagedSelector.css\";\n\nexport interface PagedSelectorOption {\n value: string;\n /** The option's name: the trigger text and the row's accessible name. */\n label: string;\n /** Drawn in the row in place of `label`, for a row richer than a string. */\n labelContent?: ReactNode;\n /** Leading visual (avatar, glyph). */\n icon?: ReactNode;\n /** Secondary line under the label. */\n description?: ReactNode;\n /** Trailing content (tokens, meta). */\n endContent?: ReactNode;\n isDisabled?: boolean;\n}\n\n/** A chosen value with its label, as `onChange` reports it. */\nexport interface PagedSelectorSelectedItem {\n value: string;\n label: string;\n}\n\ntype Passthrough = Pick<\n ComplexSelectorProps<string[]>,\n | \"label\"\n | \"isLabelHidden\"\n | \"description\"\n | \"isOptional\"\n | \"isRequired\"\n | \"isDisabled\"\n | \"isLoading\"\n | \"status\"\n | \"statusVariant\"\n | \"labelTooltip\"\n | \"size\"\n | \"variant\"\n | \"startIcon\"\n | \"width\"\n | \"placement\"\n | \"alignment\"\n | \"onOpenChange\"\n | \"containerXstyle\"\n | \"containerClassName\"\n | \"containerStyle\"\n | \"controlXstyle\"\n | \"controlClassName\"\n | \"controlStyle\"\n | \"data-testid\"\n>;\n\ninterface PagedSelectorBaseProps extends Passthrough {\n /** The loaded options, every page so far, in order. */\n options: readonly PagedSelectorOption[];\n /** Labels for selected values that may not be among `options`. */\n labels?: Readonly<Record<string, string>>;\n /**\n * Placeholder while nothing is selected.\n * @default the catalog's uic.PagedSelector.placeholder (\"Select {label}\")\n */\n placeholder?: string;\n /** Show the search box at the top of the panel. @default true */\n hasSearch?: boolean;\n /** Controlled search text; uncontrolled without it. */\n searchValue?: string;\n /** Called on every keystroke in the search box; debounce it yourself. */\n onSearchChange?: (searchValue: string) => void;\n /** @default the catalog's uic.PagedSelector.searchPlaceholder (\"Search\") */\n searchPlaceholder?: string;\n /** @default the catalog's uic.PagedSelector.searchOptions (\"Search options\") */\n searchLabel?: string;\n /** @default the catalog's uic.PagedSelector.clearSearch (\"Clear search options\") */\n clearSearchLabel?: string;\n /**\n * Called once each time the list is scrolled to within\n * `endReachedThreshold` px of its end. Load the next page here.\n */\n onEndReached?: () => void;\n /** Distance from the end, in px, that counts as reaching it. @default 30 */\n endReachedThreshold?: number;\n /** Called when the list arrives at, or leaves, its end. */\n onAtEndChange?: (isAtEnd: boolean) => void;\n /** A spinner in the foot while the next page loads. */\n isLoadingMore?: boolean;\n /** Options in all pages; shows the \"Total N items\" foot when positive. */\n totalCount?: number;\n /** @default the catalog's uic.PagedSelector.totalItems (\"Total {total} items\") */\n formatTotalCount?: (total: number) => string;\n /**\n * Replaces the empty list's content, the loading row included.\n * @default the catalog's uic.PagedSelector.loading while `isLoading`, else\n * uic.PagedSelector.noResults (\"No results\")\n */\n emptyText?: ReactNode;\n /** Above the option list, under the search box. */\n header?: ReactNode;\n /** Below the option list, in place of the total count. A function gets `close`. */\n footer?: ReactNode | ((close: () => void) => ReactNode);\n /** Height the option list scrolls within, in px. @default 260 */\n listMaxHeight?: number;\n /**\n * How a multiple selection shows in the trigger: `labels` (\"A, B, C, +2\",\n * as `MultiSelector`) or `badges` (tokens). @default \"labels\"\n */\n triggerDisplay?: \"labels\" | \"badges\";\n /** Selected items the trigger names before \"+N\". @default 3 */\n maxTriggerItems?: number;\n /** How a selected row is marked: the theme's check at its end, or a checkbox at its start. @default \"check\" */\n selectionIndicator?: \"check\" | \"checkbox\";\n /** A clear button in the trigger while something is selected. */\n hasClear?: boolean;\n /** Runs on clear. @default onChange(null) alone, onChange([]) with isMultiple */\n onClear?: () => void;\n}\n\nexport interface PagedSelectorSingleProps extends PagedSelectorBaseProps {\n isMultiple?: false;\n value?: string | null;\n onChange?: (value: string | null, item: PagedSelectorSelectedItem | null) => void;\n}\n\nexport interface PagedSelectorMultipleProps extends PagedSelectorBaseProps {\n isMultiple: true;\n value?: readonly string[];\n onChange?: (value: string[], items: PagedSelectorSelectedItem[]) => void;\n}\n\nexport type PagedSelectorProps = PagedSelectorSingleProps | PagedSelectorMultipleProps;\n\n/**\n * `ComplexSelector` insets its panel body by `--spacing-3`; this body brings\n * Selector's own gutters, so the inset goes. Astryx's compiled `padding: 0`\n * class (kmVPX3 is StyleX's key for `padding`), so no StyleX compiler is\n * needed; the test suite fails if astryx.css stops defining it.\n */\nexport const CONTENT_RESET = { padding: \"x1717udv\" } as const;\nconst contentStyles = compiledStyles({\n reset: { kmVPX3: CONTENT_RESET.padding, $$css: true },\n});\n\nexport function PagedSelector(props: PagedSelectorProps) {\n const {\n label,\n options,\n labels,\n placeholder,\n hasSearch = true,\n searchValue,\n onSearchChange,\n searchPlaceholder,\n searchLabel,\n clearSearchLabel,\n onEndReached,\n endReachedThreshold = 30,\n onAtEndChange,\n isLoadingMore,\n totalCount,\n formatTotalCount,\n emptyText,\n header,\n footer,\n listMaxHeight = 260,\n triggerDisplay = \"labels\",\n maxTriggerItems = 3,\n selectionIndicator = \"check\",\n hasClear,\n onClear,\n onOpenChange,\n isLoading,\n size,\n isMultiple,\n value: _value,\n onChange: _onChange,\n ...rest\n } = props;\n const t = useUicTranslator();\n const CheckMark = useIndicator(\"check\");\n const CheckboxMark = useIndicator(\"checkbox\");\n const listboxId = useId();\n const optionIdPrefix = useId();\n\n const selectedValues: string[] = props.isMultiple\n ? [...(props.value ?? [])]\n : props.value != null\n ? [props.value]\n : [];\n\n // Labels of every option seen, so a value picked from a page the list has\n // since dropped (a new query) keeps its name.\n const seenLabels = useRef(new Map<string, string>());\n for (const option of options) seenLabels.current.set(option.value, option.label);\n const labelOf = (value: string) =>\n labels?.[value] ??\n options.find((option) => option.value === value)?.label ??\n seenLabels.current.get(value) ??\n value;\n\n const emit = (values: string[]) => {\n const items = values.map((value) => ({ value, label: labelOf(value) }));\n if (props.isMultiple) props.onChange?.(values, items);\n else props.onChange?.(values[0] ?? null, items[0] ?? null);\n };\n\n // -1: an opened Astryx Selector highlights nothing until the pointer or an\n // arrow key picks a row.\n const [highlightedIndex, setHighlightedIndex] = useState(-1);\n const [internalSearch, setInternalSearch] = useState(\"\");\n const search = searchValue ?? internalSearch;\n const isAtEnd = useRef(false);\n\n const optionIdOf = (index: number) => `${optionIdPrefix}-option-${index}`;\n const clampedIndex =\n options.length === 0 || highlightedIndex < 0\n ? -1\n : Math.min(highlightedIndex, options.length - 1);\n // A row that went disabled under a page append does not keep the highlight.\n const activeIndex =\n clampedIndex >= 0 && options[clampedIndex]?.isDisabled ? -1 : clampedIndex;\n\n const nextEnabledIndex = (from: number, step: 1 | -1) => {\n for (let i = from; i >= 0 && i < options.length; i += step) {\n if (!options[i]?.isDisabled) return i;\n }\n return -1;\n };\n\n useLayoutEffect(() => {\n if (activeIndex < 0) return;\n document\n .getElementById(optionIdOf(activeIndex))\n ?.scrollIntoView({ block: \"nearest\" });\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [activeIndex]);\n\n const handleScroll = (event: React.UIEvent<HTMLDivElement>) => {\n if (!onEndReached && !onAtEndChange) return;\n const el = event.currentTarget;\n const isAtEndNow =\n el.scrollHeight - el.scrollTop - el.clientHeight <= endReachedThreshold;\n if (isAtEndNow === isAtEnd.current) return;\n isAtEnd.current = isAtEndNow;\n onAtEndChange?.(isAtEndNow);\n if (isAtEndNow) onEndReached?.();\n };\n\n const isSelected = (value: string) => selectedValues.includes(value);\n\n const commit = (option: PagedSelectorOption, close: () => void) => {\n if (!isMultiple) {\n emit([option.value]);\n close();\n return;\n }\n emit(\n isSelected(option.value)\n ? selectedValues.filter((value) => value !== option.value)\n : [...selectedValues, option.value],\n );\n };\n\n // Shared by the search box and, with no search box, the listbox itself.\n // Space commits only where it is not typing into the search box.\n const handleNavKeyDown = (\n event: React.KeyboardEvent<HTMLElement>,\n close: () => void,\n hasSpaceCommit: boolean,\n ) => {\n if (options.length === 0) return;\n const moveTo = (index: number) => {\n if (index >= 0) setHighlightedIndex(index);\n };\n switch (event.key) {\n case \"ArrowDown\":\n event.preventDefault();\n moveTo(nextEnabledIndex(activeIndex < 0 ? 0 : activeIndex + 1, 1));\n break;\n case \"ArrowUp\":\n event.preventDefault();\n moveTo(\n activeIndex < 0\n ? nextEnabledIndex(0, 1)\n : nextEnabledIndex(activeIndex - 1, -1),\n );\n break;\n case \"Home\":\n event.preventDefault();\n moveTo(nextEnabledIndex(0, 1));\n break;\n case \"End\":\n event.preventDefault();\n moveTo(nextEnabledIndex(options.length - 1, -1));\n break;\n case \" \":\n case \"Enter\": {\n if (event.key === \" \" && !hasSpaceCommit) break;\n const option = options[activeIndex];\n if (!option || option.isDisabled) break;\n event.preventDefault();\n commit(option, close);\n break;\n }\n default:\n break;\n }\n };\n\n const triggerLabel = (() => {\n if (selectedValues.length === 0) return undefined;\n if (!isMultiple) return labelOf(selectedValues[0]!);\n const shown = selectedValues.slice(0, maxTriggerItems);\n const remaining = selectedValues.length - shown.length;\n if (triggerDisplay === \"badges\") {\n // Display-only: the trigger is ComplexSelector's own <button>, so a\n // removable token would nest a button in a button.\n return (\n <HStack gap={0.5} vAlign=\"center\" wrap=\"wrap\">\n {shown.map((value) => (\n <Token key={value} label={labelOf(value)} size=\"sm\" />\n ))}\n {remaining > 0 ? <Text color=\"secondary\">{`+${remaining}`}</Text> : null}\n </HStack>\n );\n }\n const joined = shown.map(labelOf).join(\", \");\n return remaining > 0 ? `${joined}, +${remaining}` : joined;\n })();\n\n const totalText = (total: number) =>\n formatTotalCount?.(total) ?? t(\"uic.PagedSelector.totalItems\", { total });\n const loadingText = t(\"uic.PagedSelector.loading\");\n\n return (\n <ComplexSelector<string[]>\n {...rest}\n label={label}\n value={selectedValues}\n onChange={emit}\n triggerLabel={triggerLabel}\n placeholder={placeholder ?? t(\"uic.PagedSelector.placeholder\", { label })}\n isLoading={isLoading}\n size={size}\n hasClear={hasClear}\n onClear={onClear ?? (() => emit([]))}\n onOpenChange={(isOpen) => {\n // Astryx drops its highlight when the panel closes, so the next open\n // starts clean.\n if (!isOpen) setHighlightedIndex(-1);\n onOpenChange?.(isOpen);\n }}\n contentXstyle={contentStyles.reset}\n >\n {(_current, _commit, close) => (\n <div>\n {hasSearch ? (\n <>\n <PanelSearchInput\n label={searchLabel ?? t(\"uic.PagedSelector.searchOptions\")}\n clearLabel={clearSearchLabel ?? t(\"uic.PagedSelector.clearSearch\")}\n placeholder={\n searchPlaceholder ?? t(\"uic.PagedSelector.searchPlaceholder\")\n }\n value={search}\n onValueChange={(next) => {\n setInternalSearch(next);\n // Seeds the first selectable row, so type-then-Enter works.\n setHighlightedIndex(nextEnabledIndex(0, 1));\n onSearchChange?.(next);\n }}\n onKeyDown={(event) => handleNavKeyDown(event, close, false)}\n aria-controls={listboxId}\n aria-activedescendant={\n activeIndex >= 0 ? optionIdOf(activeIndex) : undefined\n }\n />\n <Divider />\n </>\n ) : null}\n {header}\n <div\n id={listboxId}\n role=\"listbox\"\n aria-label={label}\n aria-multiselectable={isMultiple || undefined}\n // With no search box nothing else in the panel takes focus, so the\n // popover's autofocus lands here and the arrows work.\n tabIndex={hasSearch ? undefined : 0}\n aria-activedescendant={\n !hasSearch && activeIndex >= 0 ? optionIdOf(activeIndex) : undefined\n }\n onKeyDown={\n hasSearch ? undefined : (event) => handleNavKeyDown(event, close, true)\n }\n onScroll={handleScroll}\n className=\"uic-paged-selector__listbox\"\n style={{ maxHeight: listMaxHeight }}\n >\n {options.length === 0 ? (\n <div className=\"uic-paged-selector__empty\">\n {emptyText !== undefined ? (\n emptyText\n ) : isLoading ? (\n <HStack gap={1} vAlign=\"center\" hAlign=\"center\">\n <Spinner size=\"sm\" />\n <Text color=\"secondary\">{loadingText}</Text>\n </HStack>\n ) : (\n <Text color=\"secondary\">{t(\"uic.PagedSelector.noResults\")}</Text>\n )}\n </div>\n ) : (\n options.map((option, index) => {\n const selected = isSelected(option.value);\n const isDisabled = option.isDisabled ?? false;\n return (\n <div\n key={option.value}\n id={optionIdOf(index)}\n role=\"option\"\n aria-selected={selected}\n aria-disabled={isDisabled || undefined}\n className=\"uic-paged-selector__option\"\n data-size={size === \"sm\" ? \"sm\" : undefined}\n data-selected={selected ? \"true\" : undefined}\n data-highlighted={index === activeIndex ? \"true\" : undefined}\n data-disabled={isDisabled ? \"true\" : undefined}\n onClick={() => {\n if (isDisabled) return;\n setHighlightedIndex(index);\n commit(option, close);\n }}\n onMouseEnter={() => {\n if (!isDisabled) setHighlightedIndex(index);\n }}\n >\n {selectionIndicator === \"checkbox\" && (\n <span className=\"uic-paged-selector__option-mark\">\n <CheckboxMark\n state={selected ? \"checked\" : \"unchecked\"}\n size=\"sm\"\n isDisabled={isDisabled}\n />\n </span>\n )}\n <span className=\"uic-paged-selector__option-content\">\n <SelectorOption\n icon={option.icon}\n label={option.labelContent ?? option.label}\n description={option.description}\n endContent={option.endContent}\n />\n </span>\n {/* Rendered with the state passed down: a theme that swaps\n the check for a radio draws its empty circle too. */}\n {selectionIndicator === \"check\" && (\n <span className=\"uic-paged-selector__option-mark\">\n <CheckMark\n state={selected ? \"checked\" : \"unchecked\"}\n size=\"sm\"\n isDisabled={isDisabled}\n {...themeProps(\"selector-check\")}\n />\n </span>\n )}\n </div>\n );\n })\n )}\n </div>\n {(typeof footer === \"function\" ? footer(close) : footer) ??\n (typeof totalCount === \"number\" && totalCount > 0 ? (\n <HStack\n gap={1}\n vAlign=\"center\"\n hAlign=\"end\"\n className=\"uic-paged-selector__foot\"\n >\n {isLoadingMore ? <Spinner size=\"sm\" /> : null}\n <Text color=\"secondary\" size=\"sm\">\n {totalText(totalCount)}\n </Text>\n </HStack>\n ) : null)}\n <VisuallyHidden as=\"div\" aria-live=\"polite\">\n {options.length === 0 && isLoading\n ? loadingText\n : totalText(options.length)}\n </VisuallyHidden>\n </div>\n )}\n </ComplexSelector>\n );\n}\n"],"mappings":"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"}