@anvilkit/plugin-asset-manager 0.1.5 → 0.1.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # @anvilkit/plugin-asset-manager
2
2
 
3
- > **Alpha (`0.1.3`).** Public surface may still shift before `v1.0`. Bundle budgets enforced in CI: headless ≤ 6 KB gzip, UI subpath ≤ 12 KB gzip.
3
+ > **Alpha (`0.1.6`).** Public surface may still shift before `v1.0`. Bundle budgets enforced in CI: headless ≤ 6 KB gzip, UI subpath ≤ 12 KB gzip.
4
4
 
5
5
  Headless asset manager plugin for Anvilkit Studio. The host provides the upload backend; the plugin handles validation, registration, search, IR-time resolution, CSP guidance, and (optionally) a React UI for the upload + browse experience. Designed for pluggable production backends (S3, GCS, custom HTTP) with strict trust-boundary enforcement on every adapter response.
6
6
 
@@ -10,7 +10,7 @@ Headless asset manager plugin for Anvilkit Studio. The host provides the upload
10
10
  pnpm add @anvilkit/plugin-asset-manager @anvilkit/core react react-dom @puckeditor/core
11
11
  ```
12
12
 
13
- Non-optional peers: `react ^18.2.0 || ^19.0.0`, `react-dom ^18.2.0 || ^19.0.0`, `@puckeditor/core ^0.21.2`.
13
+ Non-optional peers: `react >=19.0.0`, `react-dom >=19.0.0`, `@puckeditor/core ^0.21.2`.
14
14
 
15
15
  Subpath imports:
16
16
 
@@ -1,11 +1,15 @@
1
1
  "use strict";
2
2
  var __webpack_require__ = {};
3
3
  (()=>{
4
- __webpack_require__.d = (exports1, definition)=>{
5
- for(var key in definition)if (__webpack_require__.o(definition, key) && !__webpack_require__.o(exports1, key)) Object.defineProperty(exports1, key, {
6
- enumerable: true,
7
- get: definition[key]
8
- });
4
+ __webpack_require__.d = (exports1, getters, values)=>{
5
+ var define = (defs, kind)=>{
6
+ for(var key in defs)if (__webpack_require__.o(defs, key) && !__webpack_require__.o(exports1, key)) Object.defineProperty(exports1, key, {
7
+ enumerable: true,
8
+ [kind]: defs[key]
9
+ });
10
+ };
11
+ define(getters, "get");
12
+ define(values, "value");
9
13
  };
10
14
  })();
11
15
  (()=>{
@@ -1,11 +1,15 @@
1
1
  "use strict";
2
2
  var __webpack_require__ = {};
3
3
  (()=>{
4
- __webpack_require__.d = (exports1, definition)=>{
5
- for(var key in definition)if (__webpack_require__.o(definition, key) && !__webpack_require__.o(exports1, key)) Object.defineProperty(exports1, key, {
6
- enumerable: true,
7
- get: definition[key]
8
- });
4
+ __webpack_require__.d = (exports1, getters, values)=>{
5
+ var define = (defs, kind)=>{
6
+ for(var key in defs)if (__webpack_require__.o(defs, key) && !__webpack_require__.o(exports1, key)) Object.defineProperty(exports1, key, {
7
+ enumerable: true,
8
+ [kind]: defs[key]
9
+ });
10
+ };
11
+ define(getters, "get");
12
+ define(values, "value");
9
13
  };
10
14
  })();
11
15
  (()=>{
@@ -23,9 +27,6 @@ var __webpack_require__ = {};
23
27
  })();
24
28
  var __webpack_exports__ = {};
25
29
  __webpack_require__.r(__webpack_exports__);
26
- __webpack_require__.d(__webpack_exports__, {
27
- extractImageDimensions: ()=>extractImageDimensions
28
- });
29
30
  const DEFAULT_TIMEOUT_MS = 3000;
30
31
  async function extractImageDimensions(url, mimeType, options = {}) {
31
32
  if (!mimeType || !mimeType.startsWith("image/")) return;
@@ -69,6 +70,9 @@ async function extractImageDimensions(url, mimeType, options = {}) {
69
70
  image.src = url;
70
71
  });
71
72
  }
73
+ __webpack_require__.d(__webpack_exports__, {
74
+ extractImageDimensions: ()=>extractImageDimensions
75
+ });
72
76
  exports.extractImageDimensions = __webpack_exports__.extractImageDimensions;
73
77
  for(var __rspack_i in __webpack_exports__)if (-1 === [
74
78
  "extractImageDimensions"
@@ -1,11 +1,15 @@
1
1
  "use strict";
2
2
  var __webpack_require__ = {};
3
3
  (()=>{
4
- __webpack_require__.d = (exports1, definition)=>{
5
- for(var key in definition)if (__webpack_require__.o(definition, key) && !__webpack_require__.o(exports1, key)) Object.defineProperty(exports1, key, {
6
- enumerable: true,
7
- get: definition[key]
8
- });
4
+ __webpack_require__.d = (exports1, getters, values)=>{
5
+ var define = (defs, kind)=>{
6
+ for(var key in defs)if (__webpack_require__.o(defs, key) && !__webpack_require__.o(exports1, key)) Object.defineProperty(exports1, key, {
7
+ enumerable: true,
8
+ [kind]: defs[key]
9
+ });
10
+ };
11
+ define(getters, "get");
12
+ define(values, "value");
9
13
  };
10
14
  })();
11
15
  (()=>{
@@ -1,11 +1,15 @@
1
1
  "use strict";
2
2
  var __webpack_require__ = {};
3
3
  (()=>{
4
- __webpack_require__.d = (exports1, definition)=>{
5
- for(var key in definition)if (__webpack_require__.o(definition, key) && !__webpack_require__.o(exports1, key)) Object.defineProperty(exports1, key, {
6
- enumerable: true,
7
- get: definition[key]
8
- });
4
+ __webpack_require__.d = (exports1, getters, values)=>{
5
+ var define = (defs, kind)=>{
6
+ for(var key in defs)if (__webpack_require__.o(defs, key) && !__webpack_require__.o(exports1, key)) Object.defineProperty(exports1, key, {
7
+ enumerable: true,
8
+ [kind]: defs[key]
9
+ });
10
+ };
11
+ define(getters, "get");
12
+ define(values, "value");
9
13
  };
10
14
  })();
11
15
  (()=>{
package/dist/index.cjs CHANGED
@@ -1,11 +1,15 @@
1
1
  "use strict";
2
2
  var __webpack_require__ = {};
3
3
  (()=>{
4
- __webpack_require__.d = (exports1, definition)=>{
5
- for(var key in definition)if (__webpack_require__.o(definition, key) && !__webpack_require__.o(exports1, key)) Object.defineProperty(exports1, key, {
6
- enumerable: true,
7
- get: definition[key]
8
- });
4
+ __webpack_require__.d = (exports1, getters, values)=>{
5
+ var define = (defs, kind)=>{
6
+ for(var key in defs)if (__webpack_require__.o(defs, key) && !__webpack_require__.o(exports1, key)) Object.defineProperty(exports1, key, {
7
+ enumerable: true,
8
+ [kind]: defs[key]
9
+ });
10
+ };
11
+ define(getters, "get");
12
+ define(values, "value");
9
13
  };
10
14
  })();
11
15
  (()=>{
package/dist/plugin.cjs CHANGED
@@ -10,11 +10,15 @@ var __webpack_require__ = {};
10
10
  };
11
11
  })();
12
12
  (()=>{
13
- __webpack_require__.d = (exports1, definition)=>{
14
- for(var key in definition)if (__webpack_require__.o(definition, key) && !__webpack_require__.o(exports1, key)) Object.defineProperty(exports1, key, {
15
- enumerable: true,
16
- get: definition[key]
17
- });
13
+ __webpack_require__.d = (exports1, getters, values)=>{
14
+ var define = (defs, kind)=>{
15
+ for(var key in defs)if (__webpack_require__.o(defs, key) && !__webpack_require__.o(exports1, key)) Object.defineProperty(exports1, key, {
16
+ enumerable: true,
17
+ [kind]: defs[key]
18
+ });
19
+ };
20
+ define(getters, "get");
21
+ define(values, "value");
18
22
  };
19
23
  })();
20
24
  (()=>{
@@ -1,11 +1,15 @@
1
1
  "use strict";
2
2
  var __webpack_require__ = {};
3
3
  (()=>{
4
- __webpack_require__.d = (exports1, definition)=>{
5
- for(var key in definition)if (__webpack_require__.o(definition, key) && !__webpack_require__.o(exports1, key)) Object.defineProperty(exports1, key, {
6
- enumerable: true,
7
- get: definition[key]
8
- });
4
+ __webpack_require__.d = (exports1, getters, values)=>{
5
+ var define = (defs, kind)=>{
6
+ for(var key in defs)if (__webpack_require__.o(defs, key) && !__webpack_require__.o(exports1, key)) Object.defineProperty(exports1, key, {
7
+ enumerable: true,
8
+ [kind]: defs[key]
9
+ });
10
+ };
11
+ define(getters, "get");
12
+ define(values, "value");
9
13
  };
10
14
  })();
11
15
  (()=>{
@@ -2,11 +2,15 @@
2
2
  "use client";
3
3
  var __webpack_require__ = {};
4
4
  (()=>{
5
- __webpack_require__.d = (exports1, definition)=>{
6
- for(var key in definition)if (__webpack_require__.o(definition, key) && !__webpack_require__.o(exports1, key)) Object.defineProperty(exports1, key, {
7
- enumerable: true,
8
- get: definition[key]
9
- });
5
+ __webpack_require__.d = (exports1, getters, values)=>{
6
+ var define = (defs, kind)=>{
7
+ for(var key in defs)if (__webpack_require__.o(defs, key) && !__webpack_require__.o(exports1, key)) Object.defineProperty(exports1, key, {
8
+ enumerable: true,
9
+ [kind]: defs[key]
10
+ });
11
+ };
12
+ define(getters, "get");
13
+ define(values, "value");
10
14
  };
11
15
  })();
12
16
  (()=>{
@@ -29,8 +33,8 @@ __webpack_require__.d(__webpack_exports__, {
29
33
  });
30
34
  const card_namespaceObject = require("@anvilkit/ui/card");
31
35
  const input_namespaceObject = require("@anvilkit/ui/input");
36
+ const windowed_namespaceObject = require("@anvilkit/ui/windowed");
32
37
  const external_react_namespaceObject = require("react");
33
- const external_react_dom_namespaceObject = require("react-dom");
34
38
  const infer_kind_cjs_namespaceObject = require("../utils/infer-kind.cjs");
35
39
  const KIND_FILTERS = [
36
40
  "image",
@@ -43,20 +47,13 @@ const DEFAULT_VIRTUALIZE_THRESHOLD = 50;
43
47
  const DEFAULT_ITEM_HEIGHT = 56;
44
48
  const DEFAULT_MAX_HEIGHT = 400;
45
49
  const DEFAULT_PAGE_SIZE = 100;
46
- const OVERSCAN = 4;
47
50
  function AssetBrowser({ assets, onInsert, onDelete, onReplace, onEdit, searchEnabled = false, pageSize = DEFAULT_PAGE_SIZE, virtualizeThreshold = DEFAULT_VIRTUALIZE_THRESHOLD, itemHeight = DEFAULT_ITEM_HEIGHT, maxHeight = DEFAULT_MAX_HEIGHT }) {
48
51
  const [activeIndex, setActiveIndex] = external_react_namespaceObject.useState(assets.length > 0 ? 0 : -1);
49
- const [scrollTop, setScrollTop] = external_react_namespaceObject.useState(0);
50
52
  const [query, setQuery] = external_react_namespaceObject.useState("");
51
53
  const [activeKinds, setActiveKinds] = external_react_namespaceObject.useState([]);
52
54
  const [pageLimit, setPageLimit] = external_react_namespaceObject.useState(pageSize);
53
55
  const buttonRefs = external_react_namespaceObject.useRef([]);
54
- const scrollContainerRef = external_react_namespaceObject.useRef(null);
55
- const scrollFrameRef = external_react_namespaceObject.useRef(null);
56
- const pendingScrollTopRef = external_react_namespaceObject.useRef(0);
57
- external_react_namespaceObject.useEffect(()=>()=>{
58
- if (null !== scrollFrameRef.current && "function" == typeof cancelAnimationFrame) cancelAnimationFrame(scrollFrameRef.current);
59
- }, []);
56
+ const pendingFocusRef = external_react_namespaceObject.useRef(null);
60
57
  const searchIndex = external_react_namespaceObject.useMemo(()=>{
61
58
  if (!searchEnabled) return null;
62
59
  return assets.map((asset)=>({
@@ -96,7 +93,6 @@ function AssetBrowser({ assets, onInsert, onDelete, onReplace, onEdit, searchEna
96
93
  searchEnabled
97
94
  ]);
98
95
  const total = visibleSlice.length;
99
- const isVirtualized = total > virtualizeThreshold;
100
96
  const hasMore = searchEnabled && filteredAssets.length > visibleSlice.length;
101
97
  external_react_namespaceObject.useEffect(()=>{
102
98
  if (0 === total) return void setActiveIndex(-1);
@@ -104,31 +100,20 @@ function AssetBrowser({ assets, onInsert, onDelete, onReplace, onEdit, searchEna
104
100
  }, [
105
101
  total
106
102
  ]);
103
+ function focusRow(index) {
104
+ const node = buttonRefs.current[index];
105
+ if (node) {
106
+ node.focus();
107
+ return true;
108
+ }
109
+ return false;
110
+ }
107
111
  function moveFocus(nextIndex) {
108
112
  if (0 === total) return;
109
113
  const clampedIndex = Math.max(0, Math.min(nextIndex, total - 1));
110
- if (isVirtualized && scrollContainerRef.current) {
111
- const targetTop = clampedIndex * itemHeight;
112
- const targetBottom = targetTop + itemHeight;
113
- const viewTop = scrollContainerRef.current.scrollTop;
114
- let nextScrollTop = viewTop;
115
- if (targetTop < viewTop) nextScrollTop = targetTop;
116
- else if (targetBottom > viewTop + maxHeight) nextScrollTop = targetBottom - maxHeight;
117
- if (null !== scrollFrameRef.current && "function" == typeof cancelAnimationFrame) {
118
- cancelAnimationFrame(scrollFrameRef.current);
119
- scrollFrameRef.current = null;
120
- }
121
- pendingScrollTopRef.current = nextScrollTop;
122
- (0, external_react_dom_namespaceObject.flushSync)(()=>{
123
- setActiveIndex(clampedIndex);
124
- setScrollTop(nextScrollTop);
125
- });
126
- scrollContainerRef.current.scrollTop = nextScrollTop;
127
- buttonRefs.current[clampedIndex]?.focus();
128
- return;
129
- }
114
+ pendingFocusRef.current = clampedIndex;
130
115
  setActiveIndex(clampedIndex);
131
- buttonRefs.current[clampedIndex]?.focus();
116
+ if (focusRow(clampedIndex)) pendingFocusRef.current = null;
132
117
  }
133
118
  function toggleKind(kind) {
134
119
  setActiveKinds((current)=>current.includes(kind) ? current.filter((entry)=>entry !== kind) : [
@@ -136,16 +121,7 @@ function AssetBrowser({ assets, onInsert, onDelete, onReplace, onEdit, searchEna
136
121
  kind
137
122
  ]);
138
123
  }
139
- const firstVisible = isVirtualized ? Math.max(0, Math.floor(scrollTop / itemHeight) - OVERSCAN) : 0;
140
- const lastVisible = isVirtualized ? Math.min(total - 1, Math.ceil((scrollTop + maxHeight) / itemHeight) + OVERSCAN) : total - 1;
141
- const visibleAssets = 0 === total ? [] : isVirtualized ? visibleSlice.slice(firstVisible, lastVisible + 1) : visibleSlice;
142
- function renderRow(asset, index) {
143
- return /*#__PURE__*/ external_react_namespaceObject.createElement("li", {
144
- "aria-posinset": index + 1,
145
- "aria-setsize": total,
146
- key: asset.id,
147
- role: "listitem"
148
- }, /*#__PURE__*/ external_react_namespaceObject.createElement("button", {
124
+ const renderRow = (asset, index)=>/*#__PURE__*/ external_react_namespaceObject.createElement(external_react_namespaceObject.Fragment, null, /*#__PURE__*/ external_react_namespaceObject.createElement("button", {
149
125
  "aria-label": `Insert asset ${asset.id}`,
150
126
  onClick: ()=>{
151
127
  onInsert(asset);
@@ -181,6 +157,10 @@ function AssetBrowser({ assets, onInsert, onDelete, onReplace, onEdit, searchEna
181
157
  },
182
158
  ref: (node)=>{
183
159
  buttonRefs.current[index] = node;
160
+ if (node && pendingFocusRef.current === index) {
161
+ pendingFocusRef.current = null;
162
+ node.focus();
163
+ }
184
164
  },
185
165
  tabIndex: activeIndex === index ? 0 : -1,
186
166
  type: "button"
@@ -206,7 +186,6 @@ function AssetBrowser({ assets, onInsert, onDelete, onReplace, onEdit, searchEna
206
186
  },
207
187
  type: "button"
208
188
  }, "Delete") : null);
209
- }
210
189
  const filterRow = searchEnabled ? /*#__PURE__*/ external_react_namespaceObject.createElement("div", {
211
190
  "data-asset-manager-filters": true
212
191
  }, /*#__PURE__*/ external_react_namespaceObject.createElement(input_namespaceObject.Input, {
@@ -243,53 +222,18 @@ function AssetBrowser({ assets, onInsert, onDelete, onReplace, onEdit, searchEna
243
222
  role: "listitem"
244
223
  }, emptyLabel))));
245
224
  }
246
- if (!isVirtualized) return /*#__PURE__*/ external_react_namespaceObject.createElement(card_namespaceObject.Card, null, /*#__PURE__*/ external_react_namespaceObject.createElement(card_namespaceObject.CardHeader, null, /*#__PURE__*/ external_react_namespaceObject.createElement(card_namespaceObject.CardTitle, null, "Asset browser"), /*#__PURE__*/ external_react_namespaceObject.createElement(card_namespaceObject.CardDescription, null, "Validated assets currently registered in memory.")), /*#__PURE__*/ external_react_namespaceObject.createElement(card_namespaceObject.CardContent, null, filterRow, /*#__PURE__*/ external_react_namespaceObject.createElement("ul", {
225
+ return /*#__PURE__*/ external_react_namespaceObject.createElement(card_namespaceObject.Card, null, /*#__PURE__*/ external_react_namespaceObject.createElement(card_namespaceObject.CardHeader, null, /*#__PURE__*/ external_react_namespaceObject.createElement(card_namespaceObject.CardTitle, null, "Asset browser"), /*#__PURE__*/ external_react_namespaceObject.createElement(card_namespaceObject.CardDescription, null, "Validated assets currently registered in memory.")), /*#__PURE__*/ external_react_namespaceObject.createElement(card_namespaceObject.CardContent, null, filterRow, /*#__PURE__*/ external_react_namespaceObject.createElement(windowed_namespaceObject.Windowed, {
226
+ activeIndex: activeIndex >= 0 ? activeIndex : void 0,
247
227
  "aria-label": "Assets",
248
- role: "list"
249
- }, visibleAssets.map((asset, offset)=>renderRow(asset, offset))), hasMore ? /*#__PURE__*/ external_react_namespaceObject.createElement("button", {
250
- "data-asset-action": "load-more",
251
- onClick: ()=>{
252
- setPageLimit((current)=>current + pageSize);
253
- },
254
- type: "button"
255
- }, "Load more") : null));
256
- const totalHeight = total * itemHeight;
257
- const offsetY = firstVisible * itemHeight;
258
- return /*#__PURE__*/ external_react_namespaceObject.createElement(card_namespaceObject.Card, null, /*#__PURE__*/ external_react_namespaceObject.createElement(card_namespaceObject.CardHeader, null, /*#__PURE__*/ external_react_namespaceObject.createElement(card_namespaceObject.CardTitle, null, "Asset browser"), /*#__PURE__*/ external_react_namespaceObject.createElement(card_namespaceObject.CardDescription, null, "Validated assets currently registered in memory.")), /*#__PURE__*/ external_react_namespaceObject.createElement(card_namespaceObject.CardContent, null, filterRow, /*#__PURE__*/ external_react_namespaceObject.createElement("div", {
259
- "data-asset-manager-virtual": true,
260
- onScroll: (event)=>{
261
- const next = event.currentTarget.scrollTop;
262
- if ("function" != typeof requestAnimationFrame) return void setScrollTop(next);
263
- pendingScrollTopRef.current = next;
264
- if (null !== scrollFrameRef.current) return;
265
- scrollFrameRef.current = requestAnimationFrame(()=>{
266
- scrollFrameRef.current = null;
267
- setScrollTop(pendingScrollTopRef.current);
268
- });
269
- },
270
- ref: scrollContainerRef,
271
- style: {
272
- height: maxHeight,
273
- overflowY: "auto",
274
- position: "relative"
275
- }
276
- }, /*#__PURE__*/ external_react_namespaceObject.createElement("div", {
277
- style: {
278
- height: totalHeight,
279
- position: "relative"
280
- }
281
- }, /*#__PURE__*/ external_react_namespaceObject.createElement("ul", {
282
- "aria-label": "Assets",
283
- role: "list",
284
- style: {
285
- margin: 0,
286
- padding: 0,
287
- position: "absolute",
288
- top: offsetY,
289
- left: 0,
290
- right: 0
291
- }
292
- }, visibleAssets.map((asset, offset)=>renderRow(asset, firstVisible + offset))))), hasMore ? /*#__PURE__*/ external_react_namespaceObject.createElement("button", {
228
+ as: "ul",
229
+ "data-testid": "asset-browser-virtualized",
230
+ estimateSize: itemHeight,
231
+ items: visibleSlice,
232
+ itemKey: (asset)=>asset.id,
233
+ maxHeight: maxHeight,
234
+ renderItem: renderRow,
235
+ threshold: virtualizeThreshold
236
+ }), hasMore ? /*#__PURE__*/ external_react_namespaceObject.createElement("button", {
293
237
  "data-asset-action": "load-more",
294
238
  onClick: ()=>{
295
239
  setPageLimit((current)=>current + pageSize);
@@ -33,17 +33,16 @@ export interface AssetBrowserProps {
33
33
  /**
34
34
  * Threshold above which the list windows visible items. Below the
35
35
  * threshold the entire list renders inline so small libraries skip
36
- * scroll math entirely.
36
+ * scroll math entirely. Forwarded to the shared `Windowed` primitive.
37
37
  */
38
38
  readonly virtualizeThreshold?: number;
39
39
  /**
40
40
  * Pixel height of a single row when virtualizing.
41
41
  *
42
- * **Fixed-height contract:** the windowing math (visible range, scroll
43
- * offset, and keyboard-focus scroll) assumes every row is exactly
44
- * `itemHeight` tall. Rows that wrap or vary in height (long names,
45
- * thumbnails) will desync the scroll position and focus calculation.
46
- * Keep rows uniform, or raise `virtualizeThreshold` so the list renders
42
+ * **Fixed-height contract:** the windowing math assumes every row is
43
+ * roughly `itemHeight` tall. Rows that wrap or vary in height (long
44
+ * names, thumbnails) will desync the scroll-into-view calculation. Keep
45
+ * rows uniform, or raise `virtualizeThreshold` so the list renders
47
46
  * inline instead.
48
47
  */
49
48
  readonly itemHeight?: number;
@@ -1 +1 @@
1
- {"version":3,"file":"AssetBrowser.d.cts","sourceRoot":"","sources":["../../src/ui/AssetBrowser.tsx"],"names":[],"mappings":"AAYA,OAAO,KAAK,EAAa,YAAY,EAAE,MAAM,mBAAmB,CAAC;AAWjE,MAAM,WAAW,iBAAiB;IACjC,QAAQ,CAAC,MAAM,EAAE,SAAS,YAAY,EAAE,CAAC;IACzC,QAAQ,CAAC,QAAQ,EAAE,CAAC,KAAK,EAAE,YAAY,KAAK,IAAI,CAAC;IACjD;;;;OAIG;IACH,QAAQ,CAAC,QAAQ,CAAC,EAAE,CAAC,KAAK,EAAE,YAAY,KAAK,IAAI,CAAC;IAClD;;;OAGG;IACH,QAAQ,CAAC,SAAS,CAAC,EAAE,CAAC,KAAK,EAAE,YAAY,KAAK,IAAI,CAAC;IACnD;;;;OAIG;IACH,QAAQ,CAAC,MAAM,CAAC,EAAE,CAAC,KAAK,EAAE,YAAY,KAAK,IAAI,CAAC;IAChD;;;;OAIG;IACH,QAAQ,CAAC,aAAa,CAAC,EAAE,OAAO,CAAC;IACjC;;;OAGG;IACH,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAC3B;;;;OAIG;IACH,QAAQ,CAAC,mBAAmB,CAAC,EAAE,MAAM,CAAC;IACtC;;;;;;;;;OASG;IACH,QAAQ,CAAC,UAAU,CAAC,EAAE,MAAM,CAAC;IAC7B,8DAA8D;IAC9D,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,CAAC;CAC5B;AAQD,wBAAgB,YAAY,CAAC,EAC5B,MAAM,EACN,QAAQ,EACR,QAAQ,EACR,SAAS,EACT,MAAM,EACN,aAAqB,EACrB,QAA4B,EAC5B,mBAAkD,EAClD,UAAgC,EAChC,SAA8B,GAC9B,EAAE,iBAAiB,2CAkZnB"}
1
+ {"version":3,"file":"AssetBrowser.d.cts","sourceRoot":"","sources":["../../src/ui/AssetBrowser.tsx"],"names":[],"mappings":"AAYA,OAAO,KAAK,EAAa,YAAY,EAAE,MAAM,mBAAmB,CAAC;AAWjE,MAAM,WAAW,iBAAiB;IACjC,QAAQ,CAAC,MAAM,EAAE,SAAS,YAAY,EAAE,CAAC;IACzC,QAAQ,CAAC,QAAQ,EAAE,CAAC,KAAK,EAAE,YAAY,KAAK,IAAI,CAAC;IACjD;;;;OAIG;IACH,QAAQ,CAAC,QAAQ,CAAC,EAAE,CAAC,KAAK,EAAE,YAAY,KAAK,IAAI,CAAC;IAClD;;;OAGG;IACH,QAAQ,CAAC,SAAS,CAAC,EAAE,CAAC,KAAK,EAAE,YAAY,KAAK,IAAI,CAAC;IACnD;;;;OAIG;IACH,QAAQ,CAAC,MAAM,CAAC,EAAE,CAAC,KAAK,EAAE,YAAY,KAAK,IAAI,CAAC;IAChD;;;;OAIG;IACH,QAAQ,CAAC,aAAa,CAAC,EAAE,OAAO,CAAC;IACjC;;;OAGG;IACH,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAC3B;;;;OAIG;IACH,QAAQ,CAAC,mBAAmB,CAAC,EAAE,MAAM,CAAC;IACtC;;;;;;;;OAQG;IACH,QAAQ,CAAC,UAAU,CAAC,EAAE,MAAM,CAAC;IAC7B,8DAA8D;IAC9D,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,CAAC;CAC5B;AAOD,wBAAgB,YAAY,CAAC,EAC5B,MAAM,EACN,QAAQ,EACR,QAAQ,EACR,SAAS,EACT,MAAM,EACN,aAAqB,EACrB,QAA4B,EAC5B,mBAAkD,EAClD,UAAgC,EAChC,SAA8B,GAC9B,EAAE,iBAAiB,2CAuSnB"}
@@ -33,17 +33,16 @@ export interface AssetBrowserProps {
33
33
  /**
34
34
  * Threshold above which the list windows visible items. Below the
35
35
  * threshold the entire list renders inline so small libraries skip
36
- * scroll math entirely.
36
+ * scroll math entirely. Forwarded to the shared `Windowed` primitive.
37
37
  */
38
38
  readonly virtualizeThreshold?: number;
39
39
  /**
40
40
  * Pixel height of a single row when virtualizing.
41
41
  *
42
- * **Fixed-height contract:** the windowing math (visible range, scroll
43
- * offset, and keyboard-focus scroll) assumes every row is exactly
44
- * `itemHeight` tall. Rows that wrap or vary in height (long names,
45
- * thumbnails) will desync the scroll position and focus calculation.
46
- * Keep rows uniform, or raise `virtualizeThreshold` so the list renders
42
+ * **Fixed-height contract:** the windowing math assumes every row is
43
+ * roughly `itemHeight` tall. Rows that wrap or vary in height (long
44
+ * names, thumbnails) will desync the scroll-into-view calculation. Keep
45
+ * rows uniform, or raise `virtualizeThreshold` so the list renders
47
46
  * inline instead.
48
47
  */
49
48
  readonly itemHeight?: number;
@@ -1 +1 @@
1
- {"version":3,"file":"AssetBrowser.d.ts","sourceRoot":"","sources":["../../src/ui/AssetBrowser.tsx"],"names":[],"mappings":"AAYA,OAAO,KAAK,EAAa,YAAY,EAAE,MAAM,mBAAmB,CAAC;AAWjE,MAAM,WAAW,iBAAiB;IACjC,QAAQ,CAAC,MAAM,EAAE,SAAS,YAAY,EAAE,CAAC;IACzC,QAAQ,CAAC,QAAQ,EAAE,CAAC,KAAK,EAAE,YAAY,KAAK,IAAI,CAAC;IACjD;;;;OAIG;IACH,QAAQ,CAAC,QAAQ,CAAC,EAAE,CAAC,KAAK,EAAE,YAAY,KAAK,IAAI,CAAC;IAClD;;;OAGG;IACH,QAAQ,CAAC,SAAS,CAAC,EAAE,CAAC,KAAK,EAAE,YAAY,KAAK,IAAI,CAAC;IACnD;;;;OAIG;IACH,QAAQ,CAAC,MAAM,CAAC,EAAE,CAAC,KAAK,EAAE,YAAY,KAAK,IAAI,CAAC;IAChD;;;;OAIG;IACH,QAAQ,CAAC,aAAa,CAAC,EAAE,OAAO,CAAC;IACjC;;;OAGG;IACH,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAC3B;;;;OAIG;IACH,QAAQ,CAAC,mBAAmB,CAAC,EAAE,MAAM,CAAC;IACtC;;;;;;;;;OASG;IACH,QAAQ,CAAC,UAAU,CAAC,EAAE,MAAM,CAAC;IAC7B,8DAA8D;IAC9D,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,CAAC;CAC5B;AAQD,wBAAgB,YAAY,CAAC,EAC5B,MAAM,EACN,QAAQ,EACR,QAAQ,EACR,SAAS,EACT,MAAM,EACN,aAAqB,EACrB,QAA4B,EAC5B,mBAAkD,EAClD,UAAgC,EAChC,SAA8B,GAC9B,EAAE,iBAAiB,2CAkZnB"}
1
+ {"version":3,"file":"AssetBrowser.d.ts","sourceRoot":"","sources":["../../src/ui/AssetBrowser.tsx"],"names":[],"mappings":"AAYA,OAAO,KAAK,EAAa,YAAY,EAAE,MAAM,mBAAmB,CAAC;AAWjE,MAAM,WAAW,iBAAiB;IACjC,QAAQ,CAAC,MAAM,EAAE,SAAS,YAAY,EAAE,CAAC;IACzC,QAAQ,CAAC,QAAQ,EAAE,CAAC,KAAK,EAAE,YAAY,KAAK,IAAI,CAAC;IACjD;;;;OAIG;IACH,QAAQ,CAAC,QAAQ,CAAC,EAAE,CAAC,KAAK,EAAE,YAAY,KAAK,IAAI,CAAC;IAClD;;;OAGG;IACH,QAAQ,CAAC,SAAS,CAAC,EAAE,CAAC,KAAK,EAAE,YAAY,KAAK,IAAI,CAAC;IACnD;;;;OAIG;IACH,QAAQ,CAAC,MAAM,CAAC,EAAE,CAAC,KAAK,EAAE,YAAY,KAAK,IAAI,CAAC;IAChD;;;;OAIG;IACH,QAAQ,CAAC,aAAa,CAAC,EAAE,OAAO,CAAC;IACjC;;;OAGG;IACH,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAC3B;;;;OAIG;IACH,QAAQ,CAAC,mBAAmB,CAAC,EAAE,MAAM,CAAC;IACtC;;;;;;;;OAQG;IACH,QAAQ,CAAC,UAAU,CAAC,EAAE,MAAM,CAAC;IAC7B,8DAA8D;IAC9D,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,CAAC;CAC5B;AAOD,wBAAgB,YAAY,CAAC,EAC5B,MAAM,EACN,QAAQ,EACR,QAAQ,EACR,SAAS,EACT,MAAM,EACN,aAAqB,EACrB,QAA4B,EAC5B,mBAAkD,EAClD,UAAgC,EAChC,SAA8B,GAC9B,EAAE,iBAAiB,2CAuSnB"}
@@ -1,7 +1,7 @@
1
1
  "use client";
2
2
  import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@anvilkit/ui/card";
3
3
  import { Input } from "@anvilkit/ui/input";
4
- import { flushSync } from "react-dom";
4
+ import { Windowed } from "@anvilkit/ui/windowed";
5
5
  import { inferAssetKind } from "../utils/infer-kind.js";
6
6
  import * as __rspack_external_react from "react";
7
7
  const KIND_FILTERS = [
@@ -15,20 +15,13 @@ const DEFAULT_VIRTUALIZE_THRESHOLD = 50;
15
15
  const DEFAULT_ITEM_HEIGHT = 56;
16
16
  const DEFAULT_MAX_HEIGHT = 400;
17
17
  const DEFAULT_PAGE_SIZE = 100;
18
- const OVERSCAN = 4;
19
18
  function AssetBrowser({ assets, onInsert, onDelete, onReplace, onEdit, searchEnabled = false, pageSize = DEFAULT_PAGE_SIZE, virtualizeThreshold = DEFAULT_VIRTUALIZE_THRESHOLD, itemHeight = DEFAULT_ITEM_HEIGHT, maxHeight = DEFAULT_MAX_HEIGHT }) {
20
19
  const [activeIndex, setActiveIndex] = __rspack_external_react.useState(assets.length > 0 ? 0 : -1);
21
- const [scrollTop, setScrollTop] = __rspack_external_react.useState(0);
22
20
  const [query, setQuery] = __rspack_external_react.useState("");
23
21
  const [activeKinds, setActiveKinds] = __rspack_external_react.useState([]);
24
22
  const [pageLimit, setPageLimit] = __rspack_external_react.useState(pageSize);
25
23
  const buttonRefs = __rspack_external_react.useRef([]);
26
- const scrollContainerRef = __rspack_external_react.useRef(null);
27
- const scrollFrameRef = __rspack_external_react.useRef(null);
28
- const pendingScrollTopRef = __rspack_external_react.useRef(0);
29
- __rspack_external_react.useEffect(()=>()=>{
30
- if (null !== scrollFrameRef.current && "function" == typeof cancelAnimationFrame) cancelAnimationFrame(scrollFrameRef.current);
31
- }, []);
24
+ const pendingFocusRef = __rspack_external_react.useRef(null);
32
25
  const searchIndex = __rspack_external_react.useMemo(()=>{
33
26
  if (!searchEnabled) return null;
34
27
  return assets.map((asset)=>({
@@ -68,7 +61,6 @@ function AssetBrowser({ assets, onInsert, onDelete, onReplace, onEdit, searchEna
68
61
  searchEnabled
69
62
  ]);
70
63
  const total = visibleSlice.length;
71
- const isVirtualized = total > virtualizeThreshold;
72
64
  const hasMore = searchEnabled && filteredAssets.length > visibleSlice.length;
73
65
  __rspack_external_react.useEffect(()=>{
74
66
  if (0 === total) return void setActiveIndex(-1);
@@ -76,31 +68,20 @@ function AssetBrowser({ assets, onInsert, onDelete, onReplace, onEdit, searchEna
76
68
  }, [
77
69
  total
78
70
  ]);
71
+ function focusRow(index) {
72
+ const node = buttonRefs.current[index];
73
+ if (node) {
74
+ node.focus();
75
+ return true;
76
+ }
77
+ return false;
78
+ }
79
79
  function moveFocus(nextIndex) {
80
80
  if (0 === total) return;
81
81
  const clampedIndex = Math.max(0, Math.min(nextIndex, total - 1));
82
- if (isVirtualized && scrollContainerRef.current) {
83
- const targetTop = clampedIndex * itemHeight;
84
- const targetBottom = targetTop + itemHeight;
85
- const viewTop = scrollContainerRef.current.scrollTop;
86
- let nextScrollTop = viewTop;
87
- if (targetTop < viewTop) nextScrollTop = targetTop;
88
- else if (targetBottom > viewTop + maxHeight) nextScrollTop = targetBottom - maxHeight;
89
- if (null !== scrollFrameRef.current && "function" == typeof cancelAnimationFrame) {
90
- cancelAnimationFrame(scrollFrameRef.current);
91
- scrollFrameRef.current = null;
92
- }
93
- pendingScrollTopRef.current = nextScrollTop;
94
- flushSync(()=>{
95
- setActiveIndex(clampedIndex);
96
- setScrollTop(nextScrollTop);
97
- });
98
- scrollContainerRef.current.scrollTop = nextScrollTop;
99
- buttonRefs.current[clampedIndex]?.focus();
100
- return;
101
- }
82
+ pendingFocusRef.current = clampedIndex;
102
83
  setActiveIndex(clampedIndex);
103
- buttonRefs.current[clampedIndex]?.focus();
84
+ if (focusRow(clampedIndex)) pendingFocusRef.current = null;
104
85
  }
105
86
  function toggleKind(kind) {
106
87
  setActiveKinds((current)=>current.includes(kind) ? current.filter((entry)=>entry !== kind) : [
@@ -108,16 +89,7 @@ function AssetBrowser({ assets, onInsert, onDelete, onReplace, onEdit, searchEna
108
89
  kind
109
90
  ]);
110
91
  }
111
- const firstVisible = isVirtualized ? Math.max(0, Math.floor(scrollTop / itemHeight) - OVERSCAN) : 0;
112
- const lastVisible = isVirtualized ? Math.min(total - 1, Math.ceil((scrollTop + maxHeight) / itemHeight) + OVERSCAN) : total - 1;
113
- const visibleAssets = 0 === total ? [] : isVirtualized ? visibleSlice.slice(firstVisible, lastVisible + 1) : visibleSlice;
114
- function renderRow(asset, index) {
115
- return /*#__PURE__*/ __rspack_external_react.createElement("li", {
116
- "aria-posinset": index + 1,
117
- "aria-setsize": total,
118
- key: asset.id,
119
- role: "listitem"
120
- }, /*#__PURE__*/ __rspack_external_react.createElement("button", {
92
+ const renderRow = (asset, index)=>/*#__PURE__*/ __rspack_external_react.createElement(__rspack_external_react.Fragment, null, /*#__PURE__*/ __rspack_external_react.createElement("button", {
121
93
  "aria-label": `Insert asset ${asset.id}`,
122
94
  onClick: ()=>{
123
95
  onInsert(asset);
@@ -153,6 +125,10 @@ function AssetBrowser({ assets, onInsert, onDelete, onReplace, onEdit, searchEna
153
125
  },
154
126
  ref: (node)=>{
155
127
  buttonRefs.current[index] = node;
128
+ if (node && pendingFocusRef.current === index) {
129
+ pendingFocusRef.current = null;
130
+ node.focus();
131
+ }
156
132
  },
157
133
  tabIndex: activeIndex === index ? 0 : -1,
158
134
  type: "button"
@@ -178,7 +154,6 @@ function AssetBrowser({ assets, onInsert, onDelete, onReplace, onEdit, searchEna
178
154
  },
179
155
  type: "button"
180
156
  }, "Delete") : null);
181
- }
182
157
  const filterRow = searchEnabled ? /*#__PURE__*/ __rspack_external_react.createElement("div", {
183
158
  "data-asset-manager-filters": true
184
159
  }, /*#__PURE__*/ __rspack_external_react.createElement(Input, {
@@ -215,53 +190,18 @@ function AssetBrowser({ assets, onInsert, onDelete, onReplace, onEdit, searchEna
215
190
  role: "listitem"
216
191
  }, emptyLabel))));
217
192
  }
218
- if (!isVirtualized) return /*#__PURE__*/ __rspack_external_react.createElement(Card, null, /*#__PURE__*/ __rspack_external_react.createElement(CardHeader, null, /*#__PURE__*/ __rspack_external_react.createElement(CardTitle, null, "Asset browser"), /*#__PURE__*/ __rspack_external_react.createElement(CardDescription, null, "Validated assets currently registered in memory.")), /*#__PURE__*/ __rspack_external_react.createElement(CardContent, null, filterRow, /*#__PURE__*/ __rspack_external_react.createElement("ul", {
193
+ return /*#__PURE__*/ __rspack_external_react.createElement(Card, null, /*#__PURE__*/ __rspack_external_react.createElement(CardHeader, null, /*#__PURE__*/ __rspack_external_react.createElement(CardTitle, null, "Asset browser"), /*#__PURE__*/ __rspack_external_react.createElement(CardDescription, null, "Validated assets currently registered in memory.")), /*#__PURE__*/ __rspack_external_react.createElement(CardContent, null, filterRow, /*#__PURE__*/ __rspack_external_react.createElement(Windowed, {
194
+ activeIndex: activeIndex >= 0 ? activeIndex : void 0,
219
195
  "aria-label": "Assets",
220
- role: "list"
221
- }, visibleAssets.map((asset, offset)=>renderRow(asset, offset))), hasMore ? /*#__PURE__*/ __rspack_external_react.createElement("button", {
222
- "data-asset-action": "load-more",
223
- onClick: ()=>{
224
- setPageLimit((current)=>current + pageSize);
225
- },
226
- type: "button"
227
- }, "Load more") : null));
228
- const totalHeight = total * itemHeight;
229
- const offsetY = firstVisible * itemHeight;
230
- return /*#__PURE__*/ __rspack_external_react.createElement(Card, null, /*#__PURE__*/ __rspack_external_react.createElement(CardHeader, null, /*#__PURE__*/ __rspack_external_react.createElement(CardTitle, null, "Asset browser"), /*#__PURE__*/ __rspack_external_react.createElement(CardDescription, null, "Validated assets currently registered in memory.")), /*#__PURE__*/ __rspack_external_react.createElement(CardContent, null, filterRow, /*#__PURE__*/ __rspack_external_react.createElement("div", {
231
- "data-asset-manager-virtual": true,
232
- onScroll: (event)=>{
233
- const next = event.currentTarget.scrollTop;
234
- if ("function" != typeof requestAnimationFrame) return void setScrollTop(next);
235
- pendingScrollTopRef.current = next;
236
- if (null !== scrollFrameRef.current) return;
237
- scrollFrameRef.current = requestAnimationFrame(()=>{
238
- scrollFrameRef.current = null;
239
- setScrollTop(pendingScrollTopRef.current);
240
- });
241
- },
242
- ref: scrollContainerRef,
243
- style: {
244
- height: maxHeight,
245
- overflowY: "auto",
246
- position: "relative"
247
- }
248
- }, /*#__PURE__*/ __rspack_external_react.createElement("div", {
249
- style: {
250
- height: totalHeight,
251
- position: "relative"
252
- }
253
- }, /*#__PURE__*/ __rspack_external_react.createElement("ul", {
254
- "aria-label": "Assets",
255
- role: "list",
256
- style: {
257
- margin: 0,
258
- padding: 0,
259
- position: "absolute",
260
- top: offsetY,
261
- left: 0,
262
- right: 0
263
- }
264
- }, visibleAssets.map((asset, offset)=>renderRow(asset, firstVisible + offset))))), hasMore ? /*#__PURE__*/ __rspack_external_react.createElement("button", {
196
+ as: "ul",
197
+ "data-testid": "asset-browser-virtualized",
198
+ estimateSize: itemHeight,
199
+ items: visibleSlice,
200
+ itemKey: (asset)=>asset.id,
201
+ maxHeight: maxHeight,
202
+ renderItem: renderRow,
203
+ threshold: virtualizeThreshold
204
+ }), hasMore ? /*#__PURE__*/ __rspack_external_react.createElement("button", {
265
205
  "data-asset-action": "load-more",
266
206
  onClick: ()=>{
267
207
  setPageLimit((current)=>current + pageSize);