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

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 (105) hide show
  1. package/README.md +1 -0
  2. package/astryx/components/Board.doc.mjs +122 -0
  3. package/astryx/components/Board.tsx +3 -0
  4. package/astryx/docs/ui-common.doc.mjs +1 -1
  5. package/dist/assets/components/Board/Board.css +2 -0
  6. package/dist/components/Board/Board.d.ts +102 -0
  7. package/dist/components/Board/Board.messages.d.ts +62 -0
  8. package/dist/components/Board/Board.messages.js +68 -0
  9. package/dist/components/Board/Board.messages.js.map +1 -0
  10. package/dist/components/Board/Board2.js +607 -0
  11. package/dist/components/Board/Board2.js.map +1 -0
  12. package/dist/components/Board/engine.d.ts +60 -0
  13. package/dist/components/Board/engine.js +397 -0
  14. package/dist/components/Board/engine.js.map +1 -0
  15. package/dist/components/Board/index.d.ts +4 -0
  16. package/dist/components/Board/index.js +3 -0
  17. package/dist/components/Board/layout.d.ts +81 -0
  18. package/dist/components/Board/layout.js +67 -0
  19. package/dist/components/Board/layout.js.map +1 -0
  20. package/dist/components/Board/scroll.d.ts +38 -0
  21. package/dist/components/Board/scroll.js +42 -0
  22. package/dist/components/Board/scroll.js.map +1 -0
  23. package/dist/components/DataGrid/DataGrid2.js +128 -128
  24. package/dist/components/DataGrid/DataGrid2.js.map +1 -1
  25. package/dist/components/Form/FormItem.d.ts +1 -1
  26. package/dist/components/Modal/modalStack.d.ts +9 -8
  27. package/dist/components/Modal/modalStack.js +29 -26
  28. package/dist/components/Modal/modalStack.js.map +1 -1
  29. package/dist/components/PageHeader/PageHeader2.js +4 -4
  30. package/dist/components/PagedSelector/PagedSelector2.js +37 -37
  31. package/dist/components/PagedSelector/PagedSelector2.js.map +1 -1
  32. package/dist/components/SelectionLabel/SelectionLabel.js +4 -4
  33. package/dist/forks/Drawer/Drawer.js +71 -53
  34. package/dist/forks/Drawer/Drawer.js.map +1 -1
  35. package/dist/forks/Drawer/useDrawerDialogPresence.d.ts +6 -6
  36. package/dist/forks/Drawer/useDrawerDialogPresence.js +19 -31
  37. package/dist/forks/Drawer/useDrawerDialogPresence.js.map +1 -1
  38. package/dist/i18n/catalog.js +27 -25
  39. package/dist/i18n/catalog.js.map +1 -1
  40. package/dist/i18n/locales/de-DE.js +60 -0
  41. package/dist/i18n/locales/de-DE.js.map +1 -1
  42. package/dist/i18n/locales/el-GR.js +60 -0
  43. package/dist/i18n/locales/el-GR.js.map +1 -1
  44. package/dist/i18n/locales/es-ES.js +60 -0
  45. package/dist/i18n/locales/es-ES.js.map +1 -1
  46. package/dist/i18n/locales/fi-FI.js +60 -0
  47. package/dist/i18n/locales/fi-FI.js.map +1 -1
  48. package/dist/i18n/locales/fr-FR.js +60 -0
  49. package/dist/i18n/locales/fr-FR.js.map +1 -1
  50. package/dist/i18n/locales/id-ID.js +60 -0
  51. package/dist/i18n/locales/id-ID.js.map +1 -1
  52. package/dist/i18n/locales/it-IT.js +60 -0
  53. package/dist/i18n/locales/it-IT.js.map +1 -1
  54. package/dist/i18n/locales/ja-JP.js +60 -0
  55. package/dist/i18n/locales/ja-JP.js.map +1 -1
  56. package/dist/i18n/locales/ko-KR.js +60 -0
  57. package/dist/i18n/locales/ko-KR.js.map +1 -1
  58. package/dist/i18n/locales/mn-MN.js +60 -0
  59. package/dist/i18n/locales/mn-MN.js.map +1 -1
  60. package/dist/i18n/locales/ms-MY.js +60 -0
  61. package/dist/i18n/locales/ms-MY.js.map +1 -1
  62. package/dist/i18n/locales/pl-PL.js +60 -0
  63. package/dist/i18n/locales/pl-PL.js.map +1 -1
  64. package/dist/i18n/locales/pt-BR.js +60 -0
  65. package/dist/i18n/locales/pt-BR.js.map +1 -1
  66. package/dist/i18n/locales/pt-PT.js +60 -0
  67. package/dist/i18n/locales/pt-PT.js.map +1 -1
  68. package/dist/i18n/locales/ru-RU.js +60 -0
  69. package/dist/i18n/locales/ru-RU.js.map +1 -1
  70. package/dist/i18n/locales/th-TH.js +60 -0
  71. package/dist/i18n/locales/th-TH.js.map +1 -1
  72. package/dist/i18n/locales/tr-TR.js +60 -0
  73. package/dist/i18n/locales/tr-TR.js.map +1 -1
  74. package/dist/i18n/locales/vi-VN.js +60 -0
  75. package/dist/i18n/locales/vi-VN.js.map +1 -1
  76. package/dist/i18n/locales/zh-CN.js +60 -0
  77. package/dist/i18n/locales/zh-CN.js.map +1 -1
  78. package/dist/i18n/locales/zh-TW.js +60 -0
  79. package/dist/i18n/locales/zh-TW.js.map +1 -1
  80. package/dist/index.d.ts +2 -0
  81. package/dist/index.js +61 -59
  82. package/dist/instance.js +1 -1
  83. package/dist/ui-common-locales/de-DE.json +60 -0
  84. package/dist/ui-common-locales/el-GR.json +60 -0
  85. package/dist/ui-common-locales/en.json +60 -0
  86. package/dist/ui-common-locales/es-ES.json +60 -0
  87. package/dist/ui-common-locales/fi-FI.json +60 -0
  88. package/dist/ui-common-locales/fr-FR.json +60 -0
  89. package/dist/ui-common-locales/id-ID.json +60 -0
  90. package/dist/ui-common-locales/it-IT.json +60 -0
  91. package/dist/ui-common-locales/ja-JP.json +60 -0
  92. package/dist/ui-common-locales/ko-KR.json +60 -0
  93. package/dist/ui-common-locales/mn-MN.json +60 -0
  94. package/dist/ui-common-locales/ms-MY.json +60 -0
  95. package/dist/ui-common-locales/pl-PL.json +60 -0
  96. package/dist/ui-common-locales/pt-BR.json +60 -0
  97. package/dist/ui-common-locales/pt-PT.json +60 -0
  98. package/dist/ui-common-locales/ru-RU.json +60 -0
  99. package/dist/ui-common-locales/th-TH.json +60 -0
  100. package/dist/ui-common-locales/tr-TR.json +60 -0
  101. package/dist/ui-common-locales/vi-VN.json +60 -0
  102. package/dist/ui-common-locales/zh-CN.json +60 -0
  103. package/dist/ui-common-locales/zh-TW.json +60 -0
  104. package/exports.customs.json +1 -0
  105. package/package.json +1 -1
@@ -8,28 +8,28 @@ import { HStack as s } from "@astryxdesign/core/Stack";
8
8
  import { Text as c } from "@astryxdesign/core/Text";
9
9
  import { themeProps as te } from "@astryxdesign/core/utils";
10
10
  import { Fragment as ne, jsx as l, jsxs as u } from "react/jsx-runtime";
11
- import { Token as d } from "@astryxdesign/core/Token";
12
- import { Spinner as f } from "@astryxdesign/core/Spinner";
11
+ import { VisuallyHidden as d } from "@astryxdesign/core/VisuallyHidden";
12
+ import { Token as f } from "@astryxdesign/core/Token";
13
+ import { Spinner as p } from "@astryxdesign/core/Spinner";
13
14
  import { Divider as re } from "@astryxdesign/core/Divider";
14
- import { useIndicator as p } from "@astryxdesign/core/Indicator";
15
- import { SelectorOption as ie } from "@astryxdesign/core/Selector";
16
- import { VisuallyHidden as m } from "@astryxdesign/core/VisuallyHidden";
15
+ import { useIndicator as m } from "@astryxdesign/core/Indicator";
16
+ import { SelectorOption as h } from "@astryxdesign/core/Selector";
17
17
  //#region src/components/PagedSelector/PagedSelector.tsx
18
- var h = { padding: "x1717udv" }, g = t({ reset: {
19
- kmVPX3: h.padding,
18
+ var g = { padding: "x1717udv" }, _ = t({ reset: {
19
+ kmVPX3: g.padding,
20
20
  $$css: !0
21
21
  } });
22
- function _(t) {
23
- let { label: h, options: _, labels: ae, placeholder: oe, hasSearch: v = !0, searchValue: se, onSearchChange: ce, searchPlaceholder: le, searchLabel: y, clearSearchLabel: b, onEndReached: x, endReachedThreshold: ue = 30, onAtEndChange: S, isLoadingMore: C, totalCount: w, formatTotalCount: de, emptyText: T, header: fe, footer: E, listMaxHeight: pe = 260, triggerDisplay: me = "labels", maxTriggerItems: he = 3, selectionIndicator: D = "check", hasClear: ge, onClear: O, onOpenChange: k, isLoading: A, size: j, isMultiple: M, value: _e, onChange: ve, ...N } = t, P = e(), F = p("check"), I = p("checkbox"), L = i(), ye = i(), R = t.isMultiple ? [...t.value ?? []] : t.value == null ? [] : [t.value], z = a(/* @__PURE__ */ new Map());
24
- for (let e of _) z.current.set(e.value, e.label);
25
- let B = (e) => ae?.[e] ?? _.find((t) => t.value === e)?.label ?? z.current.get(e) ?? e, V = (e) => {
22
+ function v(t) {
23
+ let { label: g, options: v, labels: ie, placeholder: ae, hasSearch: y = !0, searchValue: oe, onSearchChange: se, searchPlaceholder: ce, searchLabel: b, clearSearchLabel: le, onEndReached: x, endReachedThreshold: ue = 30, onAtEndChange: S, isLoadingMore: C, totalCount: w, formatTotalCount: de, emptyText: T, header: fe, footer: E, listMaxHeight: pe = 260, triggerDisplay: me = "labels", maxTriggerItems: he = 3, selectionIndicator: D = "check", hasClear: ge, onClear: O, onOpenChange: k, isLoading: A, size: j, isMultiple: M, value: _e, onChange: ve, ...N } = t, P = e(), F = m("check"), I = m("checkbox"), L = i(), ye = i(), R = t.isMultiple ? [...t.value ?? []] : t.value == null ? [] : [t.value], z = a(/* @__PURE__ */ new Map());
24
+ for (let e of v) z.current.set(e.value, e.label);
25
+ let B = (e) => ie?.[e] ?? v.find((t) => t.value === e)?.label ?? z.current.get(e) ?? e, V = (e) => {
26
26
  let n = e.map((e) => ({
27
27
  value: e,
28
28
  label: B(e)
29
29
  }));
30
30
  t.isMultiple ? t.onChange?.(e, n) : t.onChange?.(e[0] ?? null, n[0] ?? null);
31
- }, [H, U] = o(-1), [be, xe] = o(""), Se = se ?? be, W = a(!1), G = (e) => `${ye}-option-${e}`, K = _.length === 0 || H < 0 ? -1 : Math.min(H, _.length - 1), q = K >= 0 && _[K]?.isDisabled ? -1 : K, J = (e, t) => {
32
- for (let n = e; n >= 0 && n < _.length; n += t) if (!_[n]?.isDisabled) return n;
31
+ }, [H, U] = o(-1), [be, xe] = o(""), Se = oe ?? be, W = a(!1), G = (e) => `${ye}-option-${e}`, K = v.length === 0 || H < 0 ? -1 : Math.min(H, v.length - 1), q = K >= 0 && v[K]?.isDisabled ? -1 : K, J = (e, t) => {
32
+ for (let n = e; n >= 0 && n < v.length; n += t) if (!v[n]?.isDisabled) return n;
33
33
  return -1;
34
34
  };
35
35
  ee(() => {
@@ -46,7 +46,7 @@ function _(t) {
46
46
  }
47
47
  V(Y(e.value) ? R.filter((t) => t !== e.value) : [...R, e.value]);
48
48
  }, Z = (e, t, n) => {
49
- if (_.length === 0) return;
49
+ if (v.length === 0) return;
50
50
  let r = (e) => {
51
51
  e >= 0 && U(e);
52
52
  };
@@ -61,12 +61,12 @@ function _(t) {
61
61
  e.preventDefault(), r(J(0, 1));
62
62
  break;
63
63
  case "End":
64
- e.preventDefault(), r(J(_.length - 1, -1));
64
+ e.preventDefault(), r(J(v.length - 1, -1));
65
65
  break;
66
66
  case " ":
67
67
  case "Enter": {
68
68
  if (e.key === " " && !n) break;
69
- let r = _[q];
69
+ let r = v[q];
70
70
  if (!r || r.isDisabled) break;
71
71
  e.preventDefault(), X(r, t);
72
72
  break;
@@ -80,7 +80,7 @@ function _(t) {
80
80
  gap: .5,
81
81
  vAlign: "center",
82
82
  wrap: "wrap",
83
- children: [e.map((e) => /* @__PURE__ */ l(d, {
83
+ children: [e.map((e) => /* @__PURE__ */ l(f, {
84
84
  label: B(e),
85
85
  size: "sm"
86
86
  }, e)), t > 0 ? /* @__PURE__ */ l(c, {
@@ -93,11 +93,11 @@ function _(t) {
93
93
  })(), Q = (e) => de?.(e) ?? P("uic.PagedSelector.totalItems", { total: e }), $ = P("uic.PagedSelector.loading");
94
94
  return /* @__PURE__ */ l(n, {
95
95
  ...N,
96
- label: h,
96
+ label: g,
97
97
  value: R,
98
98
  onChange: V,
99
99
  triggerLabel: we,
100
- placeholder: oe ?? P("uic.PagedSelector.placeholder", { label: h }),
100
+ placeholder: ae ?? P("uic.PagedSelector.placeholder", { label: g }),
101
101
  isLoading: A,
102
102
  size: j,
103
103
  hasClear: ge,
@@ -105,15 +105,15 @@ function _(t) {
105
105
  onOpenChange: (e) => {
106
106
  e || U(-1), k?.(e);
107
107
  },
108
- contentXstyle: g.reset,
108
+ contentXstyle: _.reset,
109
109
  children: (e, t, n) => /* @__PURE__ */ u("div", { children: [
110
- v ? /* @__PURE__ */ u(ne, { children: [/* @__PURE__ */ l(r, {
111
- label: y ?? P("uic.PagedSelector.searchOptions"),
112
- clearLabel: b ?? P("uic.PagedSelector.clearSearch"),
113
- placeholder: le ?? P("uic.PagedSelector.searchPlaceholder"),
110
+ y ? /* @__PURE__ */ u(ne, { children: [/* @__PURE__ */ l(r, {
111
+ label: b ?? P("uic.PagedSelector.searchOptions"),
112
+ clearLabel: le ?? P("uic.PagedSelector.clearSearch"),
113
+ placeholder: ce ?? P("uic.PagedSelector.searchPlaceholder"),
114
114
  value: Se,
115
115
  onValueChange: (e) => {
116
- xe(e), U(J(0, 1)), ce?.(e);
116
+ xe(e), U(J(0, 1)), se?.(e);
117
117
  },
118
118
  onKeyDown: (e) => Z(e, n, !1),
119
119
  "aria-controls": L,
@@ -123,21 +123,21 @@ function _(t) {
123
123
  /* @__PURE__ */ l("div", {
124
124
  id: L,
125
125
  role: "listbox",
126
- "aria-label": h,
126
+ "aria-label": g,
127
127
  "aria-multiselectable": M || void 0,
128
- tabIndex: v ? void 0 : 0,
129
- "aria-activedescendant": !v && q >= 0 ? G(q) : void 0,
130
- onKeyDown: v ? void 0 : (e) => Z(e, n, !0),
128
+ tabIndex: y ? void 0 : 0,
129
+ "aria-activedescendant": !y && q >= 0 ? G(q) : void 0,
130
+ onKeyDown: y ? void 0 : (e) => Z(e, n, !0),
131
131
  onScroll: Ce,
132
132
  className: "uic-paged-selector__listbox",
133
133
  style: { maxHeight: pe },
134
- children: _.length === 0 ? /* @__PURE__ */ l("div", {
134
+ children: v.length === 0 ? /* @__PURE__ */ l("div", {
135
135
  className: "uic-paged-selector__empty",
136
136
  children: T === void 0 ? A ? /* @__PURE__ */ u(s, {
137
137
  gap: 1,
138
138
  vAlign: "center",
139
139
  hAlign: "center",
140
- children: [/* @__PURE__ */ l(f, { size: "sm" }), /* @__PURE__ */ l(c, {
140
+ children: [/* @__PURE__ */ l(p, { size: "sm" }), /* @__PURE__ */ l(c, {
141
141
  color: "secondary",
142
142
  children: $
143
143
  })]
@@ -145,7 +145,7 @@ function _(t) {
145
145
  color: "secondary",
146
146
  children: P("uic.PagedSelector.noResults")
147
147
  }) : T
148
- }) : _.map((e, t) => {
148
+ }) : v.map((e, t) => {
149
149
  let r = Y(e.value), i = e.isDisabled ?? !1;
150
150
  return /* @__PURE__ */ u("div", {
151
151
  id: G(t),
@@ -174,7 +174,7 @@ function _(t) {
174
174
  }),
175
175
  /* @__PURE__ */ l("span", {
176
176
  className: "uic-paged-selector__option-content",
177
- children: /* @__PURE__ */ l(ie, {
177
+ children: /* @__PURE__ */ l(h, {
178
178
  icon: e.icon,
179
179
  label: e.labelContent ?? e.label,
180
180
  description: e.description,
@@ -199,22 +199,22 @@ function _(t) {
199
199
  vAlign: "center",
200
200
  hAlign: "end",
201
201
  className: "uic-paged-selector__foot",
202
- children: [C ? /* @__PURE__ */ l(f, { size: "sm" }) : null, /* @__PURE__ */ l(c, {
202
+ children: [C ? /* @__PURE__ */ l(p, { size: "sm" }) : null, /* @__PURE__ */ l(c, {
203
203
  color: "secondary",
204
204
  size: "sm",
205
205
  children: Q(w)
206
206
  })]
207
207
  }) : null),
208
- /* @__PURE__ */ l(m, {
208
+ /* @__PURE__ */ l(d, {
209
209
  as: "div",
210
210
  "aria-live": "polite",
211
- children: _.length === 0 && A ? $ : Q(_.length)
211
+ children: v.length === 0 && A ? $ : Q(v.length)
212
212
  })
213
213
  ] })
214
214
  });
215
215
  }
216
216
  //#endregion
217
- export { h as CONTENT_RESET, _ as PagedSelector };
217
+ export { g as CONTENT_RESET, v as PagedSelector };
218
218
 
219
219
  //# sourceMappingURL=PagedSelector2.js.map
220
220
  import "../../assets/components/PagedSelector/PagedSelector.css";
@@ -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 | \"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":";;;;;;;;;;;;;;;;;AA6LA,IAAa,IAAgB,EAAE,SAAS,WAAW,GAC7C,IAAgB,EAAe,EACnC,OAAO;CAAE,QAAQ,EAAc;CAAS,OAAO;AAAK,EACtD,CAAC;AAED,SAAgB,EAAc,GAA2B;CACvD,IAAM,EACJ,UACA,YACA,YACA,iBACA,eAAY,IACZ,iBACA,oBACA,uBACA,gBACA,sBACA,iBACA,0BAAsB,IACtB,kBACA,kBACA,eACA,sBACA,cACA,YACA,WACA,oBAAgB,KAChB,qBAAiB,UACjB,sBAAkB,GAClB,wBAAqB,SACrB,cACA,YACA,iBACA,cACA,SACA,eACA,OAAO,IACP,UAAU,IACV,GAAG,MACD,GACE,IAAI,EAAiB,GACrB,IAAY,EAAa,OAAO,GAChC,IAAe,EAAa,UAAU,GACtC,IAAY,EAAM,GAClB,KAAiB,EAAM,GAEvB,IAA2B,EAAM,aACnC,CAAC,GAAI,EAAM,SAAS,CAAC,CAAE,IACvB,EAAM,SAAS,OAEb,CAAC,IADD,CAAC,EAAM,KAAK,GAKZ,IAAa,kBAAO,IAAI,IAAoB,CAAC;CACnD,KAAK,IAAM,KAAU,GAAS,EAAW,QAAQ,IAAI,EAAO,OAAO,EAAO,KAAK;CAC/E,IAAM,KAAW,MACf,KAAS,MACT,EAAQ,MAAM,MAAW,EAAO,UAAU,CAAK,CAAC,EAAE,SAClD,EAAW,QAAQ,IAAI,CAAK,KAC5B,GAEI,KAAQ,MAAqB;EACjC,IAAM,IAAQ,EAAO,KAAK,OAAW;GAAE;GAAO,OAAO,EAAQ,CAAK;EAAE,EAAE;EACtE,AAAI,EAAM,aAAY,EAAM,WAAW,GAAQ,CAAK,IAC/C,EAAM,WAAW,EAAO,MAAM,MAAM,EAAM,MAAM,IAAI;CAC3D,GAIM,CAAC,GAAkB,KAAuB,EAAS,EAAE,GACrD,CAAC,IAAgB,MAAqB,EAAS,EAAE,GACjD,KAAS,MAAe,IACxB,IAAU,EAAO,EAAK,GAEtB,KAAc,MAAkB,GAAG,GAAe,UAAU,KAC5D,IACJ,EAAQ,WAAW,KAAK,IAAmB,IACvC,KACA,KAAK,IAAI,GAAkB,EAAQ,SAAS,CAAC,GAE7C,IACJ,KAAgB,KAAK,EAAQ,EAAa,EAAE,aAAa,KAAK,GAE1D,KAAoB,GAAc,MAAiB;EACvD,KAAK,IAAI,IAAI,GAAM,KAAK,KAAK,IAAI,EAAQ,QAAQ,KAAK,GACpD,IAAI,CAAC,EAAQ,EAAE,EAAE,YAAY,OAAO;EAEtC,OAAO;CACT;CAEA,SAAsB;EAChB,IAAc,KAClB,SACG,eAAe,EAAW,CAAW,CAAC,CAAC,EACtC,eAAe,EAAE,OAAO,UAAU,CAAC;CAEzC,GAAG,CAAC,CAAW,CAAC;CAEhB,IAAM,MAAgB,MAAyC;EAC7D,IAAI,CAAC,KAAgB,CAAC,GAAe;EACrC,IAAM,IAAK,EAAM,eACX,IACJ,EAAG,eAAe,EAAG,YAAY,EAAG,gBAAgB;EAClD,MAAe,EAAQ,YAC3B,EAAQ,UAAU,GAClB,IAAgB,CAAU,GACtB,KAAY,IAAe;CACjC,GAEM,KAAc,MAAkB,EAAe,SAAS,CAAK,GAE7D,KAAU,GAA6B,MAAsB;EACjE,IAAI,CAAC,GAAY;GAEf,AADA,EAAK,CAAC,EAAO,KAAK,CAAC,GACnB,EAAM;GACN;EACF;EACA,EACE,EAAW,EAAO,KAAK,IACnB,EAAe,QAAQ,MAAU,MAAU,EAAO,KAAK,IACvD,CAAC,GAAG,GAAgB,EAAO,KAAK,CACtC;CACF,GAIM,KACJ,GACA,GACA,MACG;EACH,IAAI,EAAQ,WAAW,GAAG;EAC1B,IAAM,KAAU,MAAkB;GAChC,AAAI,KAAS,KAAG,EAAoB,CAAK;EAC3C;EACA,QAAQ,EAAM,KAAd;GACE,KAAK;IAEH,AADA,EAAM,eAAe,GACrB,EAAO,EAAiB,IAAc,IAAI,IAAI,IAAc,GAAG,CAAC,CAAC;IACjE;GACF,KAAK;IAEH,AADA,EAAM,eAAe,GACrB,EACE,IAAc,IACV,EAAiB,GAAG,CAAC,IACrB,EAAiB,IAAc,GAAG,EAAE,CAC1C;IACA;GACF,KAAK;IAEH,AADA,EAAM,eAAe,GACrB,EAAO,EAAiB,GAAG,CAAC,CAAC;IAC7B;GACF,KAAK;IAEH,AADA,EAAM,eAAe,GACrB,EAAO,EAAiB,EAAQ,SAAS,GAAG,EAAE,CAAC;IAC/C;GACF,KAAK;GACL,KAAK,SAAS;IACZ,IAAI,EAAM,QAAQ,OAAO,CAAC,GAAgB;IAC1C,IAAM,IAAS,EAAQ;IACvB,IAAI,CAAC,KAAU,EAAO,YAAY;IAElC,AADA,EAAM,eAAe,GACrB,EAAO,GAAQ,CAAK;IACpB;GACF;EAGF;CACF,GAEM,YAAsB;EAC1B,IAAI,EAAe,WAAW,GAAG;EACjC,IAAI,CAAC,GAAY,OAAO,EAAQ,EAAe,EAAG;EAClD,IAAM,IAAQ,EAAe,MAAM,GAAG,EAAe,GAC/C,IAAY,EAAe,SAAS,EAAM;EAChD,IAAI,OAAmB,UAGrB,OACE,kBAAC,GAAD;GAAQ,KAAK;GAAK,QAAO;GAAS,MAAK;GAAvC,UAAA,CACG,EAAM,KAAK,MACV,kBAAC,GAAD;IAAmB,OAAO,EAAQ,CAAK;IAAG,MAAK;GAAM,GAAzC,CAAyC,CACtD,GACA,IAAY,IAAI,kBAAC,GAAD;IAAM,OAAM;IAAa,UAAA,IAAI;GAAkB,CAAA,IAAI,IAC9D;;EAGZ,IAAM,IAAS,EAAM,IAAI,CAAO,CAAC,CAAC,KAAK,IAAI;EAC3C,OAAO,IAAY,IAAI,GAAG,EAAO,KAAK,MAAc;CACtD,EAAA,CAAG,GAEG,KAAa,MACjB,KAAmB,CAAK,KAAK,EAAE,gCAAgC,EAAE,SAAM,CAAC,GACpE,IAAc,EAAE,2BAA2B;CAEjD,OACE,kBAAC,GAAD;EACE,GAAI;EACG;EACP,OAAO;EACP,UAAU;EACI;EACd,aAAa,MAAe,EAAE,iCAAiC,EAAE,SAAM,CAAC;EAC7D;EACL;EACI;EACV,SAAS,YAAkB,EAAK,CAAC,CAAC;EAClC,eAAe,MAAW;GAIxB,AADK,KAAQ,EAAoB,EAAE,GACnC,IAAe,CAAM;EACvB;EACA,eAAe,EAAc;EAE3B,WAAA,GAAU,GAAS,MACnB,kBAAC,OAAD,EAAA,UAAA;GACG,IACC,kBAAA,IAAA,EAAA,UAAA,CACE,kBAAC,GAAD;IACE,OAAO,KAAe,EAAE,iCAAiC;IACzD,YAAY,MAAoB,EAAE,+BAA+B;IACjE,aACE,MAAqB,EAAE,qCAAqC;IAE9D,OAAO;IACP,gBAAgB,MAAS;KAIvB,AAHA,GAAkB,CAAI,GAEtB,EAAoB,EAAiB,GAAG,CAAC,CAAC,GAC1C,KAAiB,CAAI;IACvB;IACA,YAAY,MAAU,EAAiB,GAAO,GAAO,EAAK;IAC1D,iBAAe;IACf,yBACE,KAAe,IAAI,EAAW,CAAW,IAAI,KAAA;GAEhD,CAAA,GACD,kBAAC,IAAD,CAAU,CAAA,CACV,EAAA,CAAA,IACA;GACH;GACD,kBAAC,OAAD;IACE,IAAI;IACJ,MAAK;IACL,cAAY;IACZ,wBAAsB,KAAc,KAAA;IAGpC,UAAU,IAAY,KAAA,IAAY;IAClC,yBACE,CAAC,KAAa,KAAe,IAAI,EAAW,CAAW,IAAI,KAAA;IAE7D,WACE,IAAY,KAAA,KAAa,MAAU,EAAiB,GAAO,GAAO,EAAI;IAExE,UAAU;IACV,WAAU;IACV,OAAO,EAAE,WAAW,GAAc;IAEjC,UAAA,EAAQ,WAAW,IAClB,kBAAC,OAAD;KAAK,WAAU;KACZ,UAAA,MAAc,KAAA,IAEX,IACF,kBAAC,GAAD;MAAQ,KAAK;MAAG,QAAO;MAAS,QAAO;MAAvC,UAAA,CACE,kBAAC,GAAD,EAAS,MAAK,KAAM,CAAA,GACpB,kBAAC,GAAD;OAAM,OAAM;OAAa,UAAA;MAAkB,CAAA,CACrC;KAER,CAAA,IAAA,kBAAC,GAAD;MAAM,OAAM;MAAa,UAAA,EAAE,6BAA6B;KAAQ,CAAA,IAPhE;IASC,CAAA,IAEL,EAAQ,KAAK,GAAQ,MAAU;KAC7B,IAAM,IAAW,EAAW,EAAO,KAAK,GAClC,IAAa,EAAO,cAAc;KACxC,OACE,kBAAC,OAAD;MAEE,IAAI,EAAW,CAAK;MACpB,MAAK;MACL,iBAAe;MACf,iBAAe,KAAc,KAAA;MAC7B,WAAU;MACV,aAAW,MAAS,OAAO,OAAO,KAAA;MAClC,iBAAe,IAAW,SAAS,KAAA;MACnC,oBAAkB,MAAU,IAAc,SAAS,KAAA;MACnD,iBAAe,IAAa,SAAS,KAAA;MACrC,eAAe;OACT,MACJ,EAAoB,CAAK,GACzB,EAAO,GAAQ,CAAK;MACtB;MACA,oBAAoB;OAClB,AAAK,KAAY,EAAoB,CAAK;MAC5C;MAlBF,UAAA;OAoBG,MAAuB,cACtB,kBAAC,QAAD;QAAM,WAAU;QACd,UAAA,kBAAC,GAAD;SACE,OAAO,IAAW,YAAY;SAC9B,MAAK;SACO;QACb,CAAA;OACG,CAAA;OAER,kBAAC,QAAD;QAAM,WAAU;QACd,UAAA,kBAAC,GAAD;SACE,MAAM,EAAO;SACb,OAAO,EAAO,gBAAgB,EAAO;SACrC,aAAa,EAAO;SACpB,YAAY,EAAO;QACpB,CAAA;OACG,CAAA;OAGL,MAAuB,WACtB,kBAAC,QAAD;QAAM,WAAU;QACd,UAAA,kBAAC,GAAD;SACE,OAAO,IAAW,YAAY;SAC9B,MAAK;SACO;SACZ,GAAI,GAAW,gBAAgB;QAChC,CAAA;OACG,CAAA;MAEL;KAhDE,GAAA,EAAO,KAgDT;IAET,CAAC;GAEA,CAAA;IACH,OAAO,KAAW,aAAa,EAAO,CAAK,IAAI,OAC9C,OAAO,KAAe,YAAY,IAAa,IAC9C,kBAAC,GAAD;IACE,KAAK;IACL,QAAO;IACP,QAAO;IACP,WAAU;IAJZ,UAAA,CAMG,IAAgB,kBAAC,GAAD,EAAS,MAAK,KAAM,CAAA,IAAI,MACzC,kBAAC,GAAD;KAAM,OAAM;KAAY,MAAK;KAC1B,UAAA,EAAU,CAAU;IACjB,CAAA,CACA;GACN,CAAA,IAAA;GACN,kBAAC,GAAD;IAAgB,IAAG;IAAM,aAAU;IAChC,UAAA,EAAQ,WAAW,KAAK,IACrB,IACA,EAAU,EAAQ,MAAM;GACd,CAAA;EACb,EAAA,CAAA;CAEQ,CAAA;AAErB"}
@@ -2,8 +2,8 @@ import { useUicTranslator as e } from "../../i18n/useUicTranslator.js";
2
2
  import { HStack as t } from "@astryxdesign/core/Stack";
3
3
  import { Text as n } from "@astryxdesign/core/Text";
4
4
  import { jsx as r, jsxs as i } from "react/jsx-runtime";
5
- import { Icon as a } from "@astryxdesign/core/Icon";
6
- import { IconButton as o } from "@astryxdesign/core/IconButton";
5
+ import { IconButton as a } from "@astryxdesign/core/IconButton";
6
+ import { Icon as o } from "@astryxdesign/core/Icon";
7
7
  //#region src/components/SelectionLabel/SelectionLabel.tsx
8
8
  function s({ count: s, onClear: c, label: l, clearLabel: u, clearIcon: d }) {
9
9
  let f = e();
@@ -12,10 +12,10 @@ function s({ count: s, onClear: c, label: l, clearLabel: u, clearIcon: d }) {
12
12
  return /* @__PURE__ */ i(t, {
13
13
  gap: 1,
14
14
  align: "center",
15
- children: [/* @__PURE__ */ r(n, { children: l ?? f("uic.SelectionLabel.selectedCount", { count: s }) }), c ? /* @__PURE__ */ r(o, {
15
+ children: [/* @__PURE__ */ r(n, { children: l ?? f("uic.SelectionLabel.selectedCount", { count: s }) }), c ? /* @__PURE__ */ r(a, {
16
16
  variant: "ghost",
17
17
  size: "sm",
18
- icon: d ?? /* @__PURE__ */ r(a, { icon: "close" }),
18
+ icon: d ?? /* @__PURE__ */ r(o, { icon: "close" }),
19
19
  label: p,
20
20
  tooltip: p,
21
21
  onClick: c
@@ -1,70 +1,88 @@
1
1
  "use client";
2
- import { ModalPortalContext as e } from "../../components/Modal/modalStack.js";
3
- import { compiledStyles as t } from "../compiled.js";
4
- import { dynamicStyles as n, styles as r } from "./Drawer.styles.js";
5
- import { useDrawerDialogPresence as i } from "./useDrawerDialogPresence.js";
6
- import { useCallback as a, useEffect as o, useRef as s, useState as c } from "react";
7
- import { overlayPaddingReset as l } from "@astryxdesign/core/Layout";
2
+ import { ModalPortalContext as e, modalZIndexBase as t } from "../../components/Modal/modalStack.js";
3
+ import { compiledStyles as n } from "../compiled.js";
4
+ import { dynamicStyles as r, styles as i } from "./Drawer.styles.js";
5
+ import { useDrawerDialogPresence as a } from "./useDrawerDialogPresence.js";
6
+ import { useCallback as o, useEffect as s, useId as ee, useRef as c, useState as l } from "react";
7
+ import { overlayPaddingReset as te } from "@astryxdesign/core/Layout";
8
8
  import { useFocusTrap as u, useMergedRefs as d, useScrollLock as f } from "@astryxdesign/core/hooks";
9
9
  import { LayerDepthProvider as p, useLayerDismissal as m } from "@astryxdesign/core/Layer";
10
- import { composeEventHandlers as h, mergeProps as g, themeProps as _ } from "@astryxdesign/core/utils";
11
- import { jsx as v } from "react/jsx-runtime";
12
- import * as y from "@stylexjs/stylex";
10
+ import { composeEventHandlers as ne, mergeProps as h, themeProps as g } from "@astryxdesign/core/utils";
11
+ import { jsx as _ } from "react/jsx-runtime";
12
+ import * as v from "@stylexjs/stylex";
13
13
  //#region src/forks/Drawer/Drawer.tsx
14
- var b = 56, x = "100dvw", S = t(r), ee = n, C = "x1iyjqo2 x2lwn1j xh8yej3 x1odjw0f x6ikm8r xish69e xx69xxh x1a148e8 x1a2a7pz";
15
- function w({ isOpen: t, onOpenChange: n, side: r = "end", width: w = 400, isFullWidthOnMobile: T = !1, label: E, hasScrim: D = !0, purpose: O = "info", children: k, xstyle: A, className: j, style: M, onClick: N, onKeyDown: P, ref: F, ...I }) {
16
- let L = s(null), { containerRef: R } = u({ isActive: t && D }), te = d(F, L, R), z = O !== "required", B = O === "info", [V, H] = c(t);
17
- t && !V && H(!0), i({
18
- dialogRef: L,
14
+ var y = 56, re = "100dvw", b = n(i), ie = r, ae = 1e3, x = /* @__PURE__ */ new Map();
15
+ function oe(e) {
16
+ let n = Math.max(999, ...x.values()), r = Math.min(n + 1, t() - 1);
17
+ return x.set(e, r), r;
18
+ }
19
+ function S(e) {
20
+ x.delete(e);
21
+ }
22
+ var C = "x1iyjqo2 x2lwn1j xh8yej3 x1odjw0f x6ikm8r xish69e xx69xxh x1a148e8 x1a2a7pz";
23
+ function w({ isOpen: t, onOpenChange: n, side: r = "end", width: i = 400, isFullWidthOnMobile: x = !1, label: w, hasScrim: T = !0, purpose: E = "info", children: D, xstyle: O, className: k, style: A, onClick: j, onKeyDown: M, ref: N, ...P }) {
24
+ let F = c(null), { containerRef: I } = u({ isActive: t && T }), L = d(N, F, I), R = E !== "required", z = E === "info", [B, V] = l(t);
25
+ t && !B && V(!0), a({
26
+ dialogRef: F,
19
27
  isOpen: t,
20
- isModal: D,
21
- setIsRendered: H
28
+ isModal: T,
29
+ setIsRendered: V
22
30
  });
23
- let [U, W] = c(null);
24
- o(() => {
25
- W(t ? L.current : null);
26
- }, [t]);
27
- let G = a(() => {
31
+ let [H, U] = l(null);
32
+ s(() => {
33
+ U(t && T ? F.current : null);
34
+ }, [t, T]);
35
+ let W = ee(), [G, K] = l(ae);
36
+ s(() => {
37
+ if (!(!B || T)) return K(oe(W)), () => S(W);
38
+ }, [
39
+ B,
40
+ T,
41
+ W
42
+ ]);
43
+ let q = o(() => {
28
44
  n(!1);
29
- }, [n]), { shouldDismissOnCloseRequest: K } = m({
30
- isActive: V,
31
- escapeBehavior: z ? "close" : "block",
32
- onDismiss: G,
33
- getContainer: () => L.current
45
+ }, [n]), { shouldDismissOnCloseRequest: J } = m({
46
+ isActive: B,
47
+ escapeBehavior: R ? "close" : "block",
48
+ onDismiss: q,
49
+ getContainer: () => F.current
34
50
  });
35
- f(t && D);
36
- let q = a((e) => {
37
- e.preventDefault(), K() && z && G();
51
+ f(t && T);
52
+ let se = o((e) => {
53
+ e.preventDefault(), J() && R && q();
38
54
  }, [
39
- z,
40
- G,
41
- K
42
- ]), J = a((e) => {
43
- e.target === e.currentTarget && D && B && n(!1);
55
+ R,
56
+ q,
57
+ J
58
+ ]), ce = o((e) => {
59
+ e.target === e.currentTarget && T && z && n(!1);
44
60
  }, [
45
- B,
46
- D,
61
+ z,
62
+ T,
47
63
  n
48
- ]), Y = typeof w == "number" ? `${w}px` : w, X = T ? x : `min(${Y}, calc(100dvw - ${b}px))`, Z = s(r);
49
- t && (Z.current = r);
50
- let Q = t ? r : Z.current, ne = Q === "start" ? S.start : S.end, re = Q === "start" ? S.startOpen : S.endOpen, { open: ie, ...$ } = I;
51
- return /* @__PURE__ */ v("dialog", {
52
- ref: te,
53
- ...g(_("drawer", { side: Q }), y.props(S.dialog, l.reset, ne, ee.inlineSize(Y, X), V && S.rendered, t && re, D && S.scrim, D && t && S.scrimOpen, A), j, M),
64
+ ]), Y = typeof i == "number" ? `${i}px` : i, le = x ? re : `min(${Y}, calc(100dvw - ${y}px))`, X = c(r);
65
+ t && (X.current = r);
66
+ let Z = t ? r : X.current, ue = Z === "start" ? b.start : b.end, Q = Z === "start" ? b.startOpen : b.endOpen, { open: de, ...$ } = P;
67
+ return /* @__PURE__ */ _("dialog", {
68
+ ref: L,
69
+ ...h(g("drawer", { side: Z }), v.props(b.dialog, te.reset, ue, ie.inlineSize(Y, le), B && b.rendered, t && Q, T && b.scrim, T && t && b.scrimOpen, O), k, T ? A : {
70
+ zIndex: G,
71
+ ...A
72
+ }),
54
73
  ...$,
55
- popover: D ? void 0 : "manual",
56
- "aria-label": E,
57
- "aria-modal": $["aria-modal"] ?? (D ? "true" : void 0),
58
- ...O === "required" && D ? { role: "alertdialog" } : void 0,
59
- onClick: h(N, J),
60
- onKeyDown: P,
61
- onCancel: q,
62
- children: /* @__PURE__ */ v(p, { children: /* @__PURE__ */ v("div", {
74
+ "aria-label": w,
75
+ "aria-modal": $["aria-modal"] ?? (T ? "true" : void 0),
76
+ ...E === "required" && T ? { role: "alertdialog" } : void 0,
77
+ onClick: ne(j, ce),
78
+ onKeyDown: M,
79
+ onCancel: se,
80
+ children: /* @__PURE__ */ _(p, { children: /* @__PURE__ */ _("div", {
63
81
  tabIndex: -1,
64
82
  className: C,
65
- children: /* @__PURE__ */ v(e, {
66
- value: U,
67
- children: k
83
+ children: /* @__PURE__ */ _(e, {
84
+ value: H,
85
+ children: D
68
86
  })
69
87
  }) })
70
88
  });
@@ -1 +1 @@
1
- {"version":3,"file":"Drawer.js","names":[],"sources":["../../../src/forks/Drawer/Drawer.tsx"],"sourcesContent":["// Copyright (c) Meta Platforms, Inc. and affiliates.\n// Modifications copyright (c) Lablup Inc.\n//\n// Forked from @astryxdesign/lab 0.6.5-canary.8701623, src/Drawer/Drawer.tsx\n// (MIT; see NOTICE). Provenance and the drift guard: src/forks/provenance.json.\n\n\"use client\";\n\n/**\n * Drawer, ui-common's copy of Astryx lab's, with two changes (not upstream):\n *\n * - **`aria-modal` passes through.** A scrimless drawer is non-modal, but a\n * consumer that restores modality by hand (its own mask and focus trap) can\n * now say so; the default is unchanged.\n * - **ui-common's `Modal` opens above it.** The drawer is top layer either\n * way: a modal `<dialog>` with a scrim (which also inerts everything\n * outside it), a manual popover without. A `Modal` portalled to the body\n * sat behind it, unreachable. The drawer provides its dialog through\n * `ModalPortalContext` while it is open, and a `Modal` inside renders\n * there and enters the top layer after it (modalStack.ts). Core's\n * `Dialog` needs none of this: it is top layer itself, and ui-common's\n * `Modal` is not.\n *\n * Escape routing is upstream's since lab 0.6.5: the drawer registers with\n * core's layer-dismissal stack, so a popover, selector or modal opened inside\n * it closes first and nested drawers close top-first. Everything else is\n * upstream's too. Its style namespaces are Astryx's compiled output\n * (src/forks/compiled.ts).\n *\n * Delete this fork, and its exports.exclude.json entry, once lab passes\n * `aria-modal` through and `Modal` no longer needs the portal host\n * (CONTRIBUTING, \"Forks of Astryx components\").\n */\n\n/*\n * Upstream's file notes, kept:\n *\n * Overlay panel for inspectors and detail views — the \"click a table row,\n * see its details\" pattern. Slides in from the inline start or end edge and\n * floats above the page content: unlike a docked panel it never reflows the\n * layout underneath, it overlays it (with or without a scrim).\n *\n * Inline axis only (start/end). Block-axis sheets are BottomSheet's job;\n * a drawer is always a full-height side panel.\n *\n * Sizing is viewport-aware: `width` is the desktop budget, and below\n * the mobile breakpoint it preserves a 56px reveal of the page behind, capped\n * by the requested width (or fills the viewport with `isFullWidthOnMobile`).\n *\n * Uses the native `<dialog>` element (same precedent as Dialog/MobileNav):\n * - `showModal()` when `hasScrim` (default) — top-layer rendering, focus\n * trapping, `::backdrop`, no z-index management.\n * - `showPopover()` when `hasScrim={false}` — non-modal top-layer overlay;\n * the page behind stays interactive (e.g. master-detail inspectors).\n *\n * Entry animation uses `@starting-style`; exit slides out before the active\n * modal-dialog or manual-popover host releases the top layer and focus returns\n * to the element that opened the drawer. React owns `display` for both legs\n * rather than a discrete `display` transition, so the panel stops painting in\n * the same commit as the host closes — see the `rendered` style for why.\n *\n * Sibling drawers use the shared layer dismissal stack for topmost-only Escape\n * handling and the browser top layer's chronological paint order.\n *\n * Dismissal follows Dialog: `purpose` decides whether Escape and a scrim click\n * request close, and Drawer renders no close button of its own. Compose\n * `DrawerHeader` with `onOpenChange` for a visible close action, as with\n * `DialogHeader`.\n *\n */\n\nimport { useCallback, useEffect, useRef, useState, type ReactNode } from \"react\";\nimport * as stylex from \"@stylexjs/stylex\";\nimport type { StyleXStyles } from \"@stylexjs/stylex\";\nimport type { BaseProps } from \"@astryxdesign/core\";\nimport type { DialogPurpose } from \"@astryxdesign/core/Dialog\";\nimport { useFocusTrap, useMergedRefs, useScrollLock } from \"@astryxdesign/core/hooks\";\nimport { LayerDepthProvider, useLayerDismissal } from \"@astryxdesign/core/Layer\";\nimport { composeEventHandlers, mergeProps, themeProps } from \"@astryxdesign/core/utils\";\nimport { overlayPaddingReset } from \"@astryxdesign/core/Layout\";\n\nimport { compiledStyles } from \"../compiled\";\nimport * as compiled from \"./Drawer.styles\";\nimport { ModalPortalContext } from \"../../components/Modal/modalStack\";\nimport { useDrawerDialogPresence } from \"./useDrawerDialogPresence\";\n\n// =============================================================================\n// Styles\n// =============================================================================\n\n// Material's established mobile drawer pattern leaves a 56dp reveal. Using\n// the same value in CSS pixels gives the overlay a stable visual relationship\n// to the page behind while the requested width remains an upper bound.\nconst MOBILE_PAGE_REVEAL = 56;\nconst MOBILE_WIDTH_FULL = \"100dvw\";\n\n// @astryxdesign/lab dist/Drawer/Drawer.js `styles` and `dynamicStyles`.\nconst styles = compiledStyles(compiled.styles);\nconst dynamicStyles = compiled.dynamicStyles as {\n inlineSize: (desktopWidth: string, mobileWidth: string) => StyleXStyles;\n};\n\n// Upstream's `content` style, which its compiler folded into this class list\n// (dist/Drawer/Drawer.js).\nconst CONTENT_CLASS_NAME =\n \"x1iyjqo2 x2lwn1j xh8yej3 x1odjw0f x6ikm8r xish69e xx69xxh x1a148e8 x1a2a7pz\";\n\n// =============================================================================\n// Types\n// =============================================================================\n\nexport interface DrawerProps extends BaseProps<HTMLDialogElement> {\n /** Ref forwarded to the root <dialog> element */\n ref?: React.Ref<HTMLDialogElement>;\n\n /**\n * Whether the drawer is open. Fully controlled — pair with `onOpenChange`.\n */\n isOpen: boolean;\n\n /**\n * Called when the drawer requests an open-state change. Escape and a scrim\n * click call it with `false` as `purpose` allows; a `DrawerHeader` close\n * button calls it when you pass the same callback to the header. The caller\n * owns the open state. When sibling drawers are open, Escape only closes the\n * top (last-opened) drawer.\n */\n onOpenChange: (isOpen: boolean) => void;\n\n /**\n * Which edge the drawer slides from.\n * - `'end'` — inline-end edge (right in LTR) — the inspector convention\n * - `'start'` — inline-start edge (left in LTR)\n * @default 'end'\n */\n side?: \"start\" | \"end\";\n\n /**\n * Desktop width budget. A number is pixels; a string is any CSS length\n * (`'50%'`, `'32rem'`). Below the mobile breakpoint (640px), this\n * remains the maximum while the drawer preserves a 56px reveal of the page\n * behind — see `isFullWidthOnMobile`.\n * @default 400\n */\n width?: number | string;\n\n /**\n * Whether the drawer covers the full viewport width on mobile\n * (below 640px) instead of preserving the default 56px reveal of the page\n * behind. The reveal makes the drawer read as an overlay rather than a\n * navigation.\n * @default false\n */\n isFullWidthOnMobile?: boolean;\n\n /**\n * Accessible label for the drawer (required — the drawer has no\n * built-in heading to derive a name from).\n */\n label: string;\n\n /**\n * Whether to render a modal scrim behind the drawer.\n * - `true` (default) — `showModal()`: top layer, focus trap, body scroll\n * lock, click-outside-to-close.\n * - `false` — `showPopover()`: non-modal top-layer overlay; the page behind\n * stays interactive. Escape still closes through the shared layer stack.\n * @default true\n */\n hasScrim?: boolean;\n\n /**\n * Configures how the drawer enables dismissals, matching Dialog.\n * - required: Disables Escape and scrim click (for mandatory flows); a\n * modal required drawer is exposed as an `alertdialog`\n * - form: Prevents scrim click, allows the Escape key\n * - info: Allows Escape and scrim click\n *\n * A non-modal drawer has no scrim, so `form` and `info` behave the same.\n * @default 'info'\n */\n purpose?: DialogPurpose;\n\n /**\n * Drawer content. Rendered inside a full-height scrollable area.\n * Focus the element with `data-autofocus` on open, if present.\n */\n children: ReactNode;\n\n /**\n * Test ID for the root element.\n */\n \"data-testid\"?: string;\n}\n\n// =============================================================================\n// Component\n// =============================================================================\n\n/**\n * An overlay panel for inspectors and detail views.\n *\n * Slides in from the logical start or end edge and floats above the page\n * using the native `<dialog>` element: modal with a scrim by default, or a\n * non-modal overlay with `hasScrim={false}` that leaves the page behind\n * interactive. `width` is the desktop budget; below 640px the panel preserves\n * a 56px page reveal without exceeding that budget (or fills the viewport\n * with `isFullWidthOnMobile`). Escape\n * closes the top-most open drawer; focus returns to the element that\n * opened it.\n *\n * Dismissal matches Dialog: `purpose` decides whether Escape and a scrim click\n * close the drawer, and `DrawerHeader` renders a close button when given\n * `onOpenChange`.\n *\n * @example\n * ```\n * const [selected, setSelected] = useState(null);\n * const handleOpenChange = isOpen => !isOpen && setSelected(null);\n * <Drawer\n * isOpen={selected != null}\n * onOpenChange={handleOpenChange}\n * label={`Details: ${selected?.name}`}>\n * <DrawerHeader title={selected?.name} onOpenChange={handleOpenChange} />\n * <HostDetails host={selected} />\n * </Drawer>\n * ```\n */\nexport function Drawer({\n isOpen,\n onOpenChange,\n side = \"end\",\n width = 400,\n isFullWidthOnMobile = false,\n label,\n hasScrim = true,\n purpose = \"info\",\n children,\n xstyle,\n className,\n style,\n onClick: onClickProp,\n onKeyDown: onKeyDownProp,\n ref,\n ...props\n}: DrawerProps) {\n const dialogRef = useRef<HTMLDialogElement>(null);\n const { containerRef: focusTrapRef } = useFocusTrap<HTMLDialogElement>({\n isActive: isOpen && hasScrim,\n });\n const mergedDialogRef = useMergedRefs(ref, dialogRef, focusTrapRef);\n // Derive dismissal behavior from purpose, as Dialog does.\n const allowEscape = purpose !== \"required\";\n const allowScrimClick = purpose === \"info\";\n // Whether the panel paints: true while open and for the whole slide-out.\n const [isRendered, setIsRendered] = useState(isOpen);\n\n // Adjusted during render, not in an effect: the panel has to be rendered in\n // the same commit that targets the open transform, or @starting-style has\n // nothing to animate from.\n if (isOpen && !isRendered) {\n setIsRendered(true);\n }\n\n useDrawerDialogPresence({\n dialogRef,\n isOpen,\n isModal: hasScrim,\n setIsRendered,\n });\n\n // ui-common: the dialog a Modal inside renders into, set only once\n // showModal() / showPopover() has run (the effect above), so the Modal's\n // popover enters the top layer after the dialog and paints above it. Both\n // hosts are top layer, so a body-portalled Modal would sit behind either.\n const [modalHost, setModalHost] = useState<HTMLDialogElement | null>(null);\n useEffect(() => {\n setModalHost(isOpen ? dialogRef.current : null);\n }, [isOpen]);\n\n const handleDismiss = useCallback(() => {\n onOpenChange(false);\n }, [onOpenChange]);\n\n // The shared stack owns Escape delivery across every overlay family. The\n // provider below gives layers opened from Drawer content a greater logical\n // depth even when they render elsewhere in the DOM or browser top layer.\n const { shouldDismissOnCloseRequest } = useLayerDismissal({\n // The native host stays present through the slide-out. Keep this entry on\n // the stack until the same commit that hides the host, so a second Escape\n // cannot fall through to a lower layer during the exit animation.\n isActive: isRendered,\n // A `required` drawer consumes Escape without closing, so the press cannot\n // fall through and dismiss a layer behind it either.\n escapeBehavior: allowEscape ? \"close\" : \"block\",\n onDismiss: handleDismiss,\n getContainer: () => dialogRef.current,\n });\n\n // Lock body scroll while a modal drawer is open (iOS Safari workaround).\n useScrollLock(isOpen && hasScrim);\n\n // A modal dialog's native cancel event is a platform close request (for\n // example Android Back). Keep controlled state authoritative and apply the\n // same top-most/IME rules as the shared Escape listener.\n const handleCancel = useCallback(\n (event: React.SyntheticEvent<HTMLDialogElement>) => {\n event.preventDefault();\n if (shouldDismissOnCloseRequest() && allowEscape) {\n handleDismiss();\n }\n },\n [allowEscape, handleDismiss, shouldDismissOnCloseRequest],\n );\n\n // Clicks on the ::backdrop target the <dialog> element itself; clicks on\n // drawer content always target a child (the content area fills the panel).\n const handleClick = useCallback(\n (event: React.MouseEvent<HTMLDialogElement>) => {\n if (event.target === event.currentTarget && hasScrim && allowScrimClick) {\n onOpenChange(false);\n }\n },\n [allowScrimClick, hasScrim, onOpenChange],\n );\n\n const widthValue = typeof width === \"number\" ? `${width}px` : width;\n const mobileWidth = isFullWidthOnMobile\n ? MOBILE_WIDTH_FULL\n : `min(${widthValue}, calc(100dvw - ${MOBILE_PAGE_REVEAL}px))`;\n\n // The side the panel is ANCHORED to, which is the side it must slide back\n // out to. Latched at open, because a consumer commonly derives `side` from\n // the same state that drives `isOpen` (`side={selected?.side ?? 'end'}`):\n // that state clears on close, so the live prop flips mid-exit and the panel\n // teleports to the other edge and slides out the wrong way. Children stay\n // mounted for the exit for the same reason; so does the anchor.\n const exitSideRef = useRef(side);\n if (isOpen) {\n exitSideRef.current = side;\n }\n const anchoredSide = isOpen ? side : exitSideRef.current;\n\n const sideStyle = anchoredSide === \"start\" ? styles.start : styles.end;\n const sideOpenStyle = anchoredSide === \"start\" ? styles.startOpen : styles.endOpen;\n\n // Filter out native `open` to prevent InvalidStateError when passed\n const { open: _open, ...safeProps } = props as Record<string, unknown>;\n\n return (\n <dialog\n ref={mergedDialogRef}\n {...mergeProps(\n themeProps(\"drawer\", { side: anchoredSide }),\n stylex.props(\n styles.dialog,\n overlayPaddingReset.reset,\n sideStyle,\n dynamicStyles.inlineSize(widthValue, mobileWidth),\n isRendered && styles.rendered,\n isOpen && sideOpenStyle,\n hasScrim && styles.scrim,\n hasScrim && isOpen && styles.scrimOpen,\n xstyle,\n ),\n className,\n style,\n )}\n {...safeProps}\n popover={hasScrim ? undefined : \"manual\"}\n aria-label={label}\n // ui-common: scrimless by itself is non-modal, but a consumer that\n // restores the modality by hand (a portal supplying its own mask and\n // focus trap) must be able to say so.\n aria-modal={\n (safeProps[\"aria-modal\"] as React.AriaAttributes[\"aria-modal\"]) ??\n (hasScrim ? \"true\" : undefined)\n }\n {...(purpose === \"required\" && hasScrim ? { role: \"alertdialog\" } : undefined)}\n onClick={composeEventHandlers(onClickProp, handleClick)}\n onKeyDown={onKeyDownProp}\n onCancel={handleCancel}\n >\n <LayerDepthProvider>\n {/* Scrollable content area — tabIndex so the dialog's focusing steps\n land on the panel body rather than the first button inside. */}\n <div tabIndex={-1} className={CONTENT_CLASS_NAME}>\n <ModalPortalContext value={modalHost}>{children}</ModalPortalContext>\n </div>\n </LayerDepthProvider>\n </dialog>\n );\n}\n\nDrawer.displayName = \"Drawer\";\n"],"mappings":";;;;;;;;;;;;;AA6FA,IAAM,IAAqB,IACrB,IAAoB,UAGpB,IAAS,EAAe,CAAe,GACvC,KAAgB,GAMhB,IACJ;AA2HF,SAAgB,EAAO,EACrB,WACA,iBACA,UAAO,OACP,WAAQ,KACR,yBAAsB,IACtB,UACA,cAAW,IACX,aAAU,QACV,aACA,WACA,cACA,UACA,SAAS,GACT,WAAW,GACX,QACA,GAAG,KACW;CACd,IAAM,IAAY,EAA0B,IAAI,GAC1C,EAAE,cAAc,MAAiB,EAAgC,EACrE,UAAU,KAAU,EACtB,CAAC,GACK,KAAkB,EAAc,GAAK,GAAW,CAAY,GAE5D,IAAc,MAAY,YAC1B,IAAkB,MAAY,QAE9B,CAAC,GAAY,KAAiB,EAAS,CAAM;CASnD,AAJI,KAAU,CAAC,KACb,EAAc,EAAI,GAGpB,EAAwB;EACtB;EACA;EACA,SAAS;EACT;CACF,CAAC;CAMD,IAAM,CAAC,GAAW,KAAgB,EAAmC,IAAI;CACzE,QAAgB;EACd,EAAa,IAAS,EAAU,UAAU,IAAI;CAChD,GAAG,CAAC,CAAM,CAAC;CAEX,IAAM,IAAgB,QAAkB;EACtC,EAAa,EAAK;CACpB,GAAG,CAAC,CAAY,CAAC,GAKX,EAAE,mCAAgC,EAAkB;EAIxD,UAAU;EAGV,gBAAgB,IAAc,UAAU;EACxC,WAAW;EACX,oBAAoB,EAAU;CAChC,CAAC;CAGD,EAAc,KAAU,CAAQ;CAKhC,IAAM,IAAe,GAClB,MAAmD;EAElD,AADA,EAAM,eAAe,GACjB,EAA4B,KAAK,KACnC,EAAc;CAElB,GACA;EAAC;EAAa;EAAe;CAA2B,CAC1D,GAIM,IAAc,GACjB,MAA+C;EAC9C,AAAI,EAAM,WAAW,EAAM,iBAAiB,KAAY,KACtD,EAAa,EAAK;CAEtB,GACA;EAAC;EAAiB;EAAU;CAAY,CAC1C,GAEM,IAAa,OAAO,KAAU,WAAW,GAAG,EAAM,MAAM,GACxD,IAAc,IAChB,IACA,OAAO,EAAW,kBAAkB,EAAmB,OAQrD,IAAc,EAAO,CAAI;CAC/B,AAAI,MACF,EAAY,UAAU;CAExB,IAAM,IAAe,IAAS,IAAO,EAAY,SAE3C,KAAY,MAAiB,UAAU,EAAO,QAAQ,EAAO,KAC7D,KAAgB,MAAiB,UAAU,EAAO,YAAY,EAAO,SAGrE,EAAE,MAAM,IAAO,GAAG,MAAc;CAEtC,OACE,kBAAC,UAAD;EACE,KAAK;EACL,GAAI,EACF,EAAW,UAAU,EAAE,MAAM,EAAa,CAAC,GAC3C,EAAO,MACL,EAAO,QACP,EAAoB,OACpB,IACA,GAAc,WAAW,GAAY,CAAW,GAChD,KAAc,EAAO,UACrB,KAAU,IACV,KAAY,EAAO,OACnB,KAAY,KAAU,EAAO,WAC7B,CACF,GACA,GACA,CACF;EACA,GAAI;EACJ,SAAS,IAAW,KAAA,IAAY;EAChC,cAAY;EAIZ,cACG,EAAU,kBACV,IAAW,SAAS,KAAA;EAEvB,GAAK,MAAY,cAAc,IAAW,EAAE,MAAM,cAAc,IAAI,KAAA;EACpE,SAAS,EAAqB,GAAa,CAAW;EACtD,WAAW;EACX,UAAU;EAEV,UAAA,kBAAC,GAAD,EAAA,UAGE,kBAAC,OAAD;GAAK,UAAU;GAAI,WAAW;GAC5B,UAAA,kBAAC,GAAD;IAAoB,OAAO;IAAY;GAA6B,CAAA;EACjE,CAAA,EACa,CAAA;CACd,CAAA;AAEZ;AAEA,EAAO,cAAc"}
1
+ {"version":3,"file":"Drawer.js","names":[],"sources":["../../../src/forks/Drawer/Drawer.tsx"],"sourcesContent":["// Copyright (c) Meta Platforms, Inc. and affiliates.\n// Modifications copyright (c) Lablup Inc.\n//\n// Forked from @astryxdesign/lab 0.6.5-canary.8701623, src/Drawer/Drawer.tsx\n// (MIT; see NOTICE). Provenance and the drift guard: src/forks/provenance.json.\n\n\"use client\";\n\n/**\n * Drawer, ui-common's copy of Astryx lab's, with three changes (not upstream):\n *\n * - **A scrimless drawer stays out of the top layer.** lab 0.6.5 opens it as\n * a manual popover; this copy keeps the `show()` + z-index it had before\n * (base 1000, later drawers above), because ui-common's `Modal` and\n * notification stack are z-index surfaces: a top-layer drawer painted over\n * a confirm or a notification opened while it was up, and the modal stack\n * then inerted the drawer it could not cover.\n * - **`aria-modal` passes through.** A scrimless drawer is non-modal, but a\n * consumer that restores modality by hand (its own mask and focus trap) can\n * now say so; the default is unchanged.\n * - **ui-common's `Modal` opens above a scrimmed drawer.** That drawer is a\n * modal `<dialog>`, top layer and inerting everything outside it, so a\n * `Modal` portalled to the body sat behind it, unreachable. The drawer\n * provides its dialog through `ModalPortalContext` while it is open and\n * modal, and a `Modal` inside renders there and enters the top layer\n * after it (modalStack.ts).\n *\n * Escape routing is upstream's since lab 0.6.5: the drawer registers with\n * core's layer-dismissal stack, so a popover, selector or modal opened inside\n * it closes first and nested drawers close top-first. Everything else is\n * upstream's too. Its style namespaces are Astryx's compiled output\n * (src/forks/compiled.ts).\n *\n * Delete this fork, and its exports.exclude.json entry, once ui-common's\n * `Modal` and notification stack are top layer themselves and lab passes\n * `aria-modal` through (CONTRIBUTING, \"Forks of Astryx components\").\n */\n\n/*\n * Upstream's file notes, kept:\n *\n * Overlay panel for inspectors and detail views — the \"click a table row,\n * see its details\" pattern. Slides in from the inline start or end edge and\n * floats above the page content: unlike a docked panel it never reflows the\n * layout underneath, it overlays it (with or without a scrim).\n *\n * Inline axis only (start/end). Block-axis sheets are BottomSheet's job;\n * a drawer is always a full-height side panel.\n *\n * Sizing is viewport-aware: `width` is the desktop budget, and below\n * the mobile breakpoint it preserves a 56px reveal of the page behind, capped\n * by the requested width (or fills the viewport with `isFullWidthOnMobile`).\n *\n * Uses the native `<dialog>` element (same precedent as Dialog/MobileNav):\n * - `showModal()` when `hasScrim` (default) — top-layer rendering, focus\n * trapping, `::backdrop`, no z-index management.\n * - `showPopover()` when `hasScrim={false}` — non-modal top-layer overlay;\n * the page behind stays interactive (e.g. master-detail inspectors).\n * (ui-common: `show()` and a z-index instead; see the header above.)\n *\n * Entry animation uses `@starting-style`; exit slides out before the active\n * modal-dialog or manual-popover host releases the top layer and focus returns\n * to the element that opened the drawer. React owns `display` for both legs\n * rather than a discrete `display` transition, so the panel stops painting in\n * the same commit as the host closes — see the `rendered` style for why.\n *\n * Sibling drawers use the shared layer dismissal stack for topmost-only Escape\n * handling and the browser top layer's chronological paint order.\n *\n * Dismissal follows Dialog: `purpose` decides whether Escape and a scrim click\n * request close, and Drawer renders no close button of its own. Compose\n * `DrawerHeader` with `onOpenChange` for a visible close action, as with\n * `DialogHeader`.\n *\n */\n\nimport { useCallback, useEffect, useId, useRef, useState, type ReactNode } from \"react\";\nimport * as stylex from \"@stylexjs/stylex\";\nimport type { StyleXStyles } from \"@stylexjs/stylex\";\nimport type { BaseProps } from \"@astryxdesign/core\";\nimport type { DialogPurpose } from \"@astryxdesign/core/Dialog\";\nimport { useFocusTrap, useMergedRefs, useScrollLock } from \"@astryxdesign/core/hooks\";\nimport { LayerDepthProvider, useLayerDismissal } from \"@astryxdesign/core/Layer\";\nimport { composeEventHandlers, mergeProps, themeProps } from \"@astryxdesign/core/utils\";\nimport { overlayPaddingReset } from \"@astryxdesign/core/Layout\";\n\nimport { compiledStyles } from \"../compiled\";\nimport * as compiled from \"./Drawer.styles\";\nimport { ModalPortalContext, modalZIndexBase } from \"../../components/Modal/modalStack\";\nimport { useDrawerDialogPresence } from \"./useDrawerDialogPresence\";\n\n// =============================================================================\n// Styles\n// =============================================================================\n\n// Material's established mobile drawer pattern leaves a 56dp reveal. Using\n// the same value in CSS pixels gives the overlay a stable visual relationship\n// to the page behind while the requested width remains an upper bound.\nconst MOBILE_PAGE_REVEAL = 56;\nconst MOBILE_WIDTH_FULL = \"100dvw\";\n\n// @astryxdesign/lab dist/Drawer/Drawer.js `styles` and `dynamicStyles`.\nconst styles = compiledStyles(compiled.styles);\nconst dynamicStyles = compiled.dynamicStyles as {\n inlineSize: (desktopWidth: string, mobileWidth: string) => StyleXStyles;\n};\n\n// ui-common: stacking for scrimless drawers, which open with `show()` and so\n// need a z-index; modal drawers rely on the top layer's chronological order.\n// Module-level; only mutated inside effects (SSR-safe). 1000 is the app-level\n// drawer convention lab used before 0.6.5. A new drawer goes one above the\n// highest OPEN one, so the value is bounded by how many are open at once, and\n// it stays below the modal band so a Modal always paints over a drawer.\nconst NON_MODAL_BASE_Z = 1000;\nconst openNonModalDrawers = new Map<string, number>();\n\nfunction registerNonModalDrawer(id: string): number {\n const highest = Math.max(NON_MODAL_BASE_Z - 1, ...openNonModalDrawers.values());\n const z = Math.min(highest + 1, modalZIndexBase() - 1);\n openNonModalDrawers.set(id, z);\n return z;\n}\n\nfunction unregisterNonModalDrawer(id: string): void {\n openNonModalDrawers.delete(id);\n}\n\n// Upstream's `content` style, which its compiler folded into this class list\n// (dist/Drawer/Drawer.js).\nconst CONTENT_CLASS_NAME =\n \"x1iyjqo2 x2lwn1j xh8yej3 x1odjw0f x6ikm8r xish69e xx69xxh x1a148e8 x1a2a7pz\";\n\n// =============================================================================\n// Types\n// =============================================================================\n\nexport interface DrawerProps extends BaseProps<HTMLDialogElement> {\n /** Ref forwarded to the root <dialog> element */\n ref?: React.Ref<HTMLDialogElement>;\n\n /**\n * Whether the drawer is open. Fully controlled — pair with `onOpenChange`.\n */\n isOpen: boolean;\n\n /**\n * Called when the drawer requests an open-state change. Escape and a scrim\n * click call it with `false` as `purpose` allows; a `DrawerHeader` close\n * button calls it when you pass the same callback to the header. The caller\n * owns the open state. When sibling drawers are open, Escape only closes the\n * top (last-opened) drawer.\n */\n onOpenChange: (isOpen: boolean) => void;\n\n /**\n * Which edge the drawer slides from.\n * - `'end'` — inline-end edge (right in LTR) — the inspector convention\n * - `'start'` — inline-start edge (left in LTR)\n * @default 'end'\n */\n side?: \"start\" | \"end\";\n\n /**\n * Desktop width budget. A number is pixels; a string is any CSS length\n * (`'50%'`, `'32rem'`). Below the mobile breakpoint (640px), this\n * remains the maximum while the drawer preserves a 56px reveal of the page\n * behind — see `isFullWidthOnMobile`.\n * @default 400\n */\n width?: number | string;\n\n /**\n * Whether the drawer covers the full viewport width on mobile\n * (below 640px) instead of preserving the default 56px reveal of the page\n * behind. The reveal makes the drawer read as an overlay rather than a\n * navigation.\n * @default false\n */\n isFullWidthOnMobile?: boolean;\n\n /**\n * Accessible label for the drawer (required — the drawer has no\n * built-in heading to derive a name from).\n */\n label: string;\n\n /**\n * Whether to render a modal scrim behind the drawer.\n * - `true` (default) — `showModal()`: top layer, focus trap, body scroll\n * lock, click-outside-to-close.\n * - `false` — `showPopover()`: non-modal top-layer overlay; the page behind\n * stays interactive. Escape still closes through the shared layer stack.\n * @default true\n */\n hasScrim?: boolean;\n\n /**\n * Configures how the drawer enables dismissals, matching Dialog.\n * - required: Disables Escape and scrim click (for mandatory flows); a\n * modal required drawer is exposed as an `alertdialog`\n * - form: Prevents scrim click, allows the Escape key\n * - info: Allows Escape and scrim click\n *\n * A non-modal drawer has no scrim, so `form` and `info` behave the same.\n * @default 'info'\n */\n purpose?: DialogPurpose;\n\n /**\n * Drawer content. Rendered inside a full-height scrollable area.\n * Focus the element with `data-autofocus` on open, if present.\n */\n children: ReactNode;\n\n /**\n * Test ID for the root element.\n */\n \"data-testid\"?: string;\n}\n\n// =============================================================================\n// Component\n// =============================================================================\n\n/**\n * An overlay panel for inspectors and detail views.\n *\n * Slides in from the logical start or end edge and floats above the page\n * using the native `<dialog>` element: modal with a scrim by default, or a\n * non-modal overlay with `hasScrim={false}` that leaves the page behind\n * interactive. `width` is the desktop budget; below 640px the panel preserves\n * a 56px page reveal without exceeding that budget (or fills the viewport\n * with `isFullWidthOnMobile`). Escape\n * closes the top-most open drawer; focus returns to the element that\n * opened it.\n *\n * Dismissal matches Dialog: `purpose` decides whether Escape and a scrim click\n * close the drawer, and `DrawerHeader` renders a close button when given\n * `onOpenChange`.\n *\n * @example\n * ```\n * const [selected, setSelected] = useState(null);\n * const handleOpenChange = isOpen => !isOpen && setSelected(null);\n * <Drawer\n * isOpen={selected != null}\n * onOpenChange={handleOpenChange}\n * label={`Details: ${selected?.name}`}>\n * <DrawerHeader title={selected?.name} onOpenChange={handleOpenChange} />\n * <HostDetails host={selected} />\n * </Drawer>\n * ```\n */\nexport function Drawer({\n isOpen,\n onOpenChange,\n side = \"end\",\n width = 400,\n isFullWidthOnMobile = false,\n label,\n hasScrim = true,\n purpose = \"info\",\n children,\n xstyle,\n className,\n style,\n onClick: onClickProp,\n onKeyDown: onKeyDownProp,\n ref,\n ...props\n}: DrawerProps) {\n const dialogRef = useRef<HTMLDialogElement>(null);\n const { containerRef: focusTrapRef } = useFocusTrap<HTMLDialogElement>({\n isActive: isOpen && hasScrim,\n });\n const mergedDialogRef = useMergedRefs(ref, dialogRef, focusTrapRef);\n // Derive dismissal behavior from purpose, as Dialog does.\n const allowEscape = purpose !== \"required\";\n const allowScrimClick = purpose === \"info\";\n // Whether the panel paints: true while open and for the whole slide-out.\n const [isRendered, setIsRendered] = useState(isOpen);\n\n // Adjusted during render, not in an effect: the panel has to be rendered in\n // the same commit that targets the open transform, or @starting-style has\n // nothing to animate from.\n if (isOpen && !isRendered) {\n setIsRendered(true);\n }\n\n useDrawerDialogPresence({\n dialogRef,\n isOpen,\n isModal: hasScrim,\n setIsRendered,\n });\n\n // ui-common: the dialog a Modal inside renders into, set only once\n // showModal() has run (the effect above), so the Modal's popover enters the\n // top layer after the dialog and paints above it. A scrimless drawer is not\n // top layer (`show()`), so a body-portalled Modal already stacks above it.\n const [modalHost, setModalHost] = useState<HTMLDialogElement | null>(null);\n useEffect(() => {\n setModalHost(isOpen && hasScrim ? dialogRef.current : null);\n }, [isOpen, hasScrim]);\n\n // ui-common: a scrimless drawer's z-index, last opened on top.\n const drawerId = useId();\n const [stackZ, setStackZ] = useState(NON_MODAL_BASE_Z);\n // Held while the panel paints, slide-out included, so a drawer opened during\n // another's exit goes above it rather than taking the same value.\n useEffect(() => {\n if (!isRendered || hasScrim) return;\n setStackZ(registerNonModalDrawer(drawerId));\n return () => unregisterNonModalDrawer(drawerId);\n }, [isRendered, hasScrim, drawerId]);\n\n const handleDismiss = useCallback(() => {\n onOpenChange(false);\n }, [onOpenChange]);\n\n // The shared stack owns Escape delivery across every overlay family. The\n // provider below gives layers opened from Drawer content a greater logical\n // depth even when they render elsewhere in the DOM or browser top layer.\n const { shouldDismissOnCloseRequest } = useLayerDismissal({\n // The native host stays present through the slide-out. Keep this entry on\n // the stack until the same commit that hides the host, so a second Escape\n // cannot fall through to a lower layer during the exit animation.\n isActive: isRendered,\n // A `required` drawer consumes Escape without closing, so the press cannot\n // fall through and dismiss a layer behind it either.\n escapeBehavior: allowEscape ? \"close\" : \"block\",\n onDismiss: handleDismiss,\n getContainer: () => dialogRef.current,\n });\n\n // Lock body scroll while a modal drawer is open (iOS Safari workaround).\n useScrollLock(isOpen && hasScrim);\n\n // A modal dialog's native cancel event is a platform close request (for\n // example Android Back). Keep controlled state authoritative and apply the\n // same top-most/IME rules as the shared Escape listener.\n const handleCancel = useCallback(\n (event: React.SyntheticEvent<HTMLDialogElement>) => {\n event.preventDefault();\n if (shouldDismissOnCloseRequest() && allowEscape) {\n handleDismiss();\n }\n },\n [allowEscape, handleDismiss, shouldDismissOnCloseRequest],\n );\n\n // Clicks on the ::backdrop target the <dialog> element itself; clicks on\n // drawer content always target a child (the content area fills the panel).\n const handleClick = useCallback(\n (event: React.MouseEvent<HTMLDialogElement>) => {\n if (event.target === event.currentTarget && hasScrim && allowScrimClick) {\n onOpenChange(false);\n }\n },\n [allowScrimClick, hasScrim, onOpenChange],\n );\n\n const widthValue = typeof width === \"number\" ? `${width}px` : width;\n const mobileWidth = isFullWidthOnMobile\n ? MOBILE_WIDTH_FULL\n : `min(${widthValue}, calc(100dvw - ${MOBILE_PAGE_REVEAL}px))`;\n\n // The side the panel is ANCHORED to, which is the side it must slide back\n // out to. Latched at open, because a consumer commonly derives `side` from\n // the same state that drives `isOpen` (`side={selected?.side ?? 'end'}`):\n // that state clears on close, so the live prop flips mid-exit and the panel\n // teleports to the other edge and slides out the wrong way. Children stay\n // mounted for the exit for the same reason; so does the anchor.\n const exitSideRef = useRef(side);\n if (isOpen) {\n exitSideRef.current = side;\n }\n const anchoredSide = isOpen ? side : exitSideRef.current;\n\n const sideStyle = anchoredSide === \"start\" ? styles.start : styles.end;\n const sideOpenStyle = anchoredSide === \"start\" ? styles.startOpen : styles.endOpen;\n\n // Filter out native `open` to prevent InvalidStateError when passed\n const { open: _open, ...safeProps } = props as Record<string, unknown>;\n\n return (\n <dialog\n ref={mergedDialogRef}\n {...mergeProps(\n themeProps(\"drawer\", { side: anchoredSide }),\n stylex.props(\n styles.dialog,\n overlayPaddingReset.reset,\n sideStyle,\n dynamicStyles.inlineSize(widthValue, mobileWidth),\n isRendered && styles.rendered,\n isOpen && sideOpenStyle,\n hasScrim && styles.scrim,\n hasScrim && isOpen && styles.scrimOpen,\n xstyle,\n ),\n className,\n // ui-common: the consumer's style may still override the stack.\n hasScrim ? style : { zIndex: stackZ, ...style },\n )}\n {...safeProps}\n aria-label={label}\n // ui-common: scrimless by itself is non-modal, but a consumer that\n // restores the modality by hand (a portal supplying its own mask and\n // focus trap) must be able to say so.\n aria-modal={\n (safeProps[\"aria-modal\"] as React.AriaAttributes[\"aria-modal\"]) ??\n (hasScrim ? \"true\" : undefined)\n }\n {...(purpose === \"required\" && hasScrim ? { role: \"alertdialog\" } : undefined)}\n onClick={composeEventHandlers(onClickProp, handleClick)}\n onKeyDown={onKeyDownProp}\n onCancel={handleCancel}\n >\n <LayerDepthProvider>\n {/* Scrollable content area — tabIndex so the dialog's focusing steps\n land on the panel body rather than the first button inside. */}\n <div tabIndex={-1} className={CONTENT_CLASS_NAME}>\n <ModalPortalContext value={modalHost}>{children}</ModalPortalContext>\n </div>\n </LayerDepthProvider>\n </dialog>\n );\n}\n\nDrawer.displayName = \"Drawer\";\n"],"mappings":"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"}
@@ -8,13 +8,13 @@ type UseDrawerDialogPresenceOptions = {
8
8
  /**
9
9
  * Coordinates the native dialog and React-rendered presence for Drawer.
10
10
  *
11
- * Opening captures the trigger, enters the modal-dialog or manual-popover host,
11
+ * Opening captures the trigger, opens the dialog (`showModal()` or `show()`),
12
12
  * and honours the component's `data-autofocus` contract. Closing waits for the
13
- * actual transform transition (with a computed-duration backstop), then leaves
14
- * the native host and synchronously hides the panel before the browser can paint
15
- * it outside the top layer, restores focus to the captured trigger, and only
16
- * then dispatches the popover host's synthetic `close`. Unmount cleanup closes
17
- * a host left open by React Activity or a removed subtree.
13
+ * actual transform transition (with a computed-duration backstop), then calls
14
+ * `close()` (which fires the native `close` event) and synchronously hides the
15
+ * panel before the browser can paint it outside the top layer, then restores
16
+ * focus to the captured trigger. Unmount cleanup closes a dialog left open by
17
+ * React Activity or a removed subtree.
18
18
  */
19
19
  export declare function useDrawerDialogPresence({ dialogRef, isOpen, isModal, setIsRendered, }: UseDrawerDialogPresenceOptions): void;
20
20
  export {};