@parallelworks/ui 0.52.0 → 0.53.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (55) hide show
  1. package/README.md +42 -3
  2. package/dist/ai/ai.css +5 -3
  3. package/dist/ai/components/agent/AgentMessageParts.js +1 -1
  4. package/dist/ai/strings.js +0 -1
  5. package/dist/ai/ui/sidebar.js +1 -1
  6. package/dist/components/Field.d.ts +5 -4
  7. package/dist/components/Field.js +27 -4
  8. package/dist/components/InfoTip.d.ts +12 -4
  9. package/dist/components/InfoTip.js +13 -3
  10. package/dist/components/MarkdownImpl.d.ts +1 -0
  11. package/dist/components/MarkdownImpl.js +1 -0
  12. package/dist/components/Popover.d.ts +4 -1
  13. package/dist/components/Popover.js +2 -1
  14. package/dist/components/Provider.d.ts +2 -0
  15. package/dist/components/Provider.js +1 -0
  16. package/dist/components/focus.d.ts +5 -0
  17. package/dist/components/focus.js +21 -1
  18. package/dist/editor/Editor.js +13 -3
  19. package/dist/editor/Monaco.d.ts +8 -1
  20. package/dist/editor/Monaco.js +16 -3
  21. package/dist/editor/editorModel.d.ts +1 -1
  22. package/dist/editor/editorModel.js +1 -1
  23. package/dist/editor/index.d.ts +1 -0
  24. package/dist/editor/index.js +2 -1
  25. package/dist/editor/languages.d.ts +24 -0
  26. package/dist/editor/languages.js +31 -0
  27. package/dist/editor/monacoFeatures.d.ts +7 -0
  28. package/dist/editor/monacoFeatures.js +19 -0
  29. package/dist/editor/themes.js +7 -2
  30. package/dist/editor/workers.d.ts +3 -3
  31. package/dist/editor/workers.js +8 -4
  32. package/dist/editor/yamlLanguage.js +55 -38
  33. package/dist/file-explorer/FileExplorer.d.ts +18 -4
  34. package/dist/file-explorer/FileExplorer.js +62 -19
  35. package/dist/file-explorer/FilePreview/Viewer.js +12 -8
  36. package/dist/file-explorer/FileRow.d.ts +6 -2
  37. package/dist/file-explorer/FileRow.js +35 -17
  38. package/dist/file-explorer/FileRowsWindow.d.ts +2 -1
  39. package/dist/file-explorer/FileRowsWindow.js +2 -1
  40. package/dist/file-explorer/FileTable.d.ts +3 -1
  41. package/dist/file-explorer/FileTable.js +3 -2
  42. package/dist/file-explorer/TreeView.d.ts +5 -1
  43. package/dist/file-explorer/TreeView.js +49 -25
  44. package/dist/file-explorer/helpers.js +3 -3
  45. package/dist/file-explorer/lib/utils.d.ts +3 -0
  46. package/dist/file-explorer/lib/utils.js +6 -1
  47. package/dist/form/Wizard/utils.js +2 -1
  48. package/dist/list/ActionToolbar.js +3 -2
  49. package/dist/list/ListPage.js +1 -1
  50. package/dist/list/ListTable.d.ts +5 -0
  51. package/dist/list/ListTable.js +6 -2
  52. package/dist/styles.css +1 -1
  53. package/package.json +5 -2
  54. package/dist/file-explorer/FilePreview/CodePreviewEditor.d.ts +0 -6
  55. package/dist/file-explorer/FilePreview/CodePreviewEditor.js +0 -64
@@ -54,8 +54,13 @@ function editorTheme(style, mode) {
54
54
  "editor.lineHighlightBackground": c("--theme-hover"),
55
55
  "editorLineNumber.foreground": c("--theme-placeholder"),
56
56
  "editorLineNumber.activeForeground": c("--theme-muted-text-color"),
57
- "editor.selectionBackground": c("--theme-accent-subtle"),
58
- "editor.inactiveSelectionBackground": c("--theme-hover")
57
+ ...mode === "dark" ? {
58
+ "editor.selectionBackground": `${c("--theme-accent")}66`,
59
+ "editor.inactiveSelectionBackground": `${c("--theme-accent")}40`
60
+ } : {
61
+ "editor.selectionBackground": c("--theme-accent-subtle"),
62
+ "editor.inactiveSelectionBackground": c("--theme-hover")
63
+ }
59
64
  }
60
65
  };
61
66
  }
@@ -1,7 +1,7 @@
1
1
  /**
2
- * Points window.MonacoEnvironment at the bundled workers. Idempotent, and a
3
- * host that already configured its own MonacoEnvironment wins — the editor
4
- * calls this on first load only when nothing is configured.
2
+ * Points window.MonacoEnvironment at the bundled workers and those a host added with
3
+ * `addEditorLanguages`. Idempotent, and a host that already configured its own
4
+ * MonacoEnvironment wins — the editor calls this on first load only when nothing is configured.
5
5
  */
6
6
  export declare function setupMonacoWorkers(): void;
7
7
  /**
@@ -1,20 +1,24 @@
1
+ import { hostEditorWorker } from "./languages.js";
1
2
  //#region src/editor/workers.ts
2
3
  var monacoEnvironment = { getWorker(_moduleId, label) {
4
+ const host = hostEditorWorker(label);
5
+ if (host) return host();
3
6
  if (label === "json") return new Worker(new URL(
4
7
  /* @vite-ignore */
5
8
  new URL("../assets/json.worker-BKkHJbNB.js", import.meta.url).href,
6
9
  "" + import.meta.url
7
10
  ), { type: "module" });
8
- return new Worker(new URL(
11
+ if (label === "editorWorkerService") return new Worker(new URL(
9
12
  /* @vite-ignore */
10
13
  new URL("../assets/editor.worker-Cc3TfJ_-.js", import.meta.url).href,
11
14
  "" + import.meta.url
12
15
  ), { type: "module" });
16
+ throw new Error(`The editor has no worker for Monaco's "${label}" service: pass one to addEditorLanguages.`);
13
17
  } };
14
18
  /**
15
- * Points window.MonacoEnvironment at the bundled workers. Idempotent, and a
16
- * host that already configured its own MonacoEnvironment wins — the editor
17
- * calls this on first load only when nothing is configured.
19
+ * Points window.MonacoEnvironment at the bundled workers and those a host added with
20
+ * `addEditorLanguages`. Idempotent, and a host that already configured its own
21
+ * MonacoEnvironment wins — the editor calls this on first load only when nothing is configured.
18
22
  */
19
23
  function setupMonacoWorkers() {
20
24
  if (window.MonacoEnvironment) return;
@@ -1,6 +1,6 @@
1
1
  import { createYamlWorker } from "./workers.js";
2
2
  import { onYamlSchemas } from "./yaml.js";
3
- import * as monaco from "monaco-editor";
3
+ import * as monaco from "monaco-editor/editor/editor.api";
4
4
  //#region src/editor/yamlLanguage.ts
5
5
  var LANGUAGE_ID = "yaml";
6
6
  var MARKER_OWNER = "yaml";
@@ -186,41 +186,59 @@ function answerServer(method, params) {
186
186
  return null;
187
187
  }
188
188
  function startYamlLanguage() {
189
- const worker = createYamlWorker();
190
- worker.postMessage({});
191
- const connection = new WorkerConnection(worker, answerServer);
189
+ let server;
190
+ let schemas = [];
192
191
  const open = /* @__PURE__ */ new Map();
193
- const ready = connection.request("initialize", {
194
- processId: null,
195
- rootUri: null,
196
- capabilities: {
197
- workspace: { configuration: true },
198
- textDocument: {
199
- synchronization: {},
200
- completion: { completionItem: {
201
- snippetSupport: true,
202
- insertReplaceSupport: true,
203
- documentationFormat: ["markdown", "plaintext"]
204
- } },
205
- hover: { contentFormat: ["markdown", "plaintext"] },
206
- publishDiagnostics: {}
207
- }
208
- }
209
- }).then(() => {
210
- connection.notify("initialized", {});
211
- connection.notify("workspace/didChangeConfiguration", { settings: null });
212
- });
213
- onYamlSchemas((schemas) => {
214
- ready.then(() => connection.notify("json/schemaAssociations", [schemas]));
215
- });
216
- connection.onNotification("textDocument/publishDiagnostics", (params) => {
217
- const { uri, diagnostics } = params;
218
- const model = open.get(uri);
219
- if (model && !model.isDisposed()) monaco.editor.setModelMarkers(model, MARKER_OWNER, diagnostics.map(toMarker));
192
+ function connect() {
193
+ if (server) return server;
194
+ const worker = createYamlWorker();
195
+ worker.postMessage({});
196
+ const connection = new WorkerConnection(worker, answerServer);
197
+ connection.onNotification("textDocument/publishDiagnostics", (params) => {
198
+ const { uri, diagnostics } = params;
199
+ const model = open.get(uri);
200
+ if (model && !model.isDisposed()) monaco.editor.setModelMarkers(model, MARKER_OWNER, diagnostics.map(toMarker));
201
+ });
202
+ server = {
203
+ connection,
204
+ ready: connection.request("initialize", {
205
+ processId: null,
206
+ rootUri: null,
207
+ capabilities: {
208
+ workspace: { configuration: true },
209
+ textDocument: {
210
+ synchronization: {},
211
+ completion: { completionItem: {
212
+ snippetSupport: true,
213
+ insertReplaceSupport: true,
214
+ documentationFormat: ["markdown", "plaintext"]
215
+ } },
216
+ hover: { contentFormat: ["markdown", "plaintext"] },
217
+ publishDiagnostics: {}
218
+ }
219
+ }
220
+ }).then(() => {
221
+ connection.notify("initialized", {});
222
+ connection.notify("workspace/didChangeConfiguration", { settings: null });
223
+ sendSchemas();
224
+ })
225
+ };
226
+ return server;
227
+ }
228
+ function notify(method, params) {
229
+ const { connection, ready } = connect();
230
+ ready.then(() => connection.notify(method, params));
231
+ }
232
+ function sendSchemas() {
233
+ server?.connection.notify("json/schemaAssociations", [schemas]);
234
+ }
235
+ onYamlSchemas((added) => {
236
+ schemas = added;
237
+ server?.ready.then(sendSchemas);
220
238
  });
221
239
  function close(uri, model) {
222
240
  open.delete(uri);
223
- ready.then(() => connection.notify("textDocument/didClose", { textDocument: { uri } }));
241
+ notify("textDocument/didClose", { textDocument: { uri } });
224
242
  if (!model.isDisposed()) monaco.editor.setModelMarkers(model, MARKER_OWNER, []);
225
243
  }
226
244
  function track(model) {
@@ -229,26 +247,24 @@ function startYamlLanguage() {
229
247
  const isYaml = model.getLanguageId() === LANGUAGE_ID;
230
248
  if (isYaml && !open.has(uri)) {
231
249
  open.set(uri, model);
232
- const textDocument = {
250
+ notify("textDocument/didOpen", { textDocument: {
233
251
  uri,
234
252
  languageId: LANGUAGE_ID,
235
253
  version: model.getVersionId(),
236
254
  text: model.getValue()
237
- };
238
- ready.then(() => connection.notify("textDocument/didOpen", { textDocument }));
255
+ } });
239
256
  } else if (!isYaml && open.has(uri)) close(uri, model);
240
257
  };
241
258
  model.onDidChangeLanguage(sync);
242
259
  model.onDidChangeContent(() => {
243
260
  if (!open.has(uri)) return;
244
- const params = {
261
+ notify("textDocument/didChange", {
245
262
  textDocument: {
246
263
  uri,
247
264
  version: model.getVersionId()
248
265
  },
249
266
  contentChanges: [{ text: model.getValue() }]
250
- };
251
- ready.then(() => connection.notify("textDocument/didChange", params));
267
+ });
252
268
  });
253
269
  model.onWillDispose(() => {
254
270
  if (open.get(uri) === model) close(uri, model);
@@ -258,6 +274,7 @@ function startYamlLanguage() {
258
274
  for (const model of monaco.editor.getModels()) track(model);
259
275
  monaco.editor.onDidCreateModel(track);
260
276
  async function request(method, model, params, token) {
277
+ const { connection, ready } = connect();
261
278
  await ready;
262
279
  return connection.request(method, {
263
280
  textDocument: { uri: model.uri.toString() },
@@ -2,7 +2,22 @@ import { type ReactNode } from 'react';
2
2
  import { type RowAction } from '../list/index.js';
3
3
  import type { IFileExplorerClient, IFileExplorerProvider } from './lib/fileExplorer.js';
4
4
  import type { ExplorerObject, TreeNode, TStorage } from './lib/types.js';
5
- interface IFileExplorerProps {
5
+ /** How the explorer moves between folders and files. */
6
+ type ExplorerNavigation = {
7
+ /** The URL of a path, for a host that keeps the path in its URL. Rows, the
8
+ * tree, the trail and each row menu's "Open folder" are then links through
9
+ * the host's router (`useLink`), so a path opens in a new tab, shows its
10
+ * URL and can be copied, and back and forward move between paths. */
11
+ pathHref: (path: string) => string;
12
+ /** Moves to a path without a link: from the keyboard, a delete, an upload.
13
+ * Absent, the explorer goes to `pathHref(path)` through the provider's
14
+ * `navigation.goTo`. */
15
+ onPathChange?: ((path: string) => void) | undefined;
16
+ } | {
17
+ pathHref?: undefined;
18
+ onPathChange: (path: string) => void;
19
+ };
20
+ type IFileExplorerProps = ExplorerNavigation & {
6
21
  storages: TStorage[];
7
22
  /** Resolves the storage's injected provider pair; the host owns caching. */
8
23
  getProviderAndClient: (storage: TStorage) => Promise<{
@@ -21,7 +36,6 @@ interface IFileExplorerProps {
21
36
  }[] | undefined;
22
37
  groupsLoading?: boolean;
23
38
  selectedPath: string;
24
- onPathChange: (path: string) => void;
25
39
  /** Renders only the tree, for a host that embeds it as a navigator and owns
26
40
  * the pane beside it (the notebooks page puts its editor there). The host
27
41
  * also owns the width, so the tree fills its container instead of carrying
@@ -48,6 +62,6 @@ interface IFileExplorerProps {
48
62
  extraRowActions?: ((node: TreeNode, storage: TStorage | undefined) => RowAction[]) | undefined;
49
63
  /** Host content for the selected folder's header, before its badges. */
50
64
  headerAccessory?: ((node: TreeNode, storage: TStorage | undefined) => ReactNode) | undefined;
51
- }
52
- export default function FileExplorer({ storages, onRefresh, refreshKey, onOpenFile, showUserHierarchy, groups, groupsLoading, selectedPath, onPathChange, getProviderAndClient, treeOnly, initialExpandedPaths, onExpandedPathsChange, objectUris, downloadCommand, extraRowActions, headerAccessory, }: IFileExplorerProps): import("react").JSX.Element;
65
+ };
66
+ export default function FileExplorer({ storages, onRefresh, refreshKey, onOpenFile, showUserHierarchy, groups, groupsLoading, selectedPath, onPathChange, pathHref, getProviderAndClient, treeOnly, initialExpandedPaths, onExpandedPathsChange, objectUris, downloadCommand, extraRowActions, headerAccessory, }: IFileExplorerProps): import("react").JSX.Element;
53
67
  export {};
@@ -7,7 +7,8 @@ import { Loader } from "../components/Loader.js";
7
7
  import { toast } from "../components/Toast.js";
8
8
  import { keyedByContent } from "../components/keys.js";
9
9
  import { Tooltip } from "../components/Tooltip.js";
10
- import { usePersistedState, useSlots, useStrings } from "../components/Provider.js";
10
+ import { useNavigation, usePersistedState, useSlots, useStrings } from "../components/Provider.js";
11
+ import { AppLink } from "../components/AppLink.js";
11
12
  import { useCopy } from "../components/useCopy.js";
12
13
  import { Button } from "../components/Button.js";
13
14
  import { Callout } from "../components/Callout.js";
@@ -67,6 +68,13 @@ function UriCard({ value }) {
67
68
  ]
68
69
  });
69
70
  }
71
+ var breadcrumbClass = "max-w-[16rem] cursor-pointer truncate text-(--theme-muted-text-color) transition-colors hover:text-(--theme-app)";
72
+ /** `href` resolved against the page, as the browser's "Copy link" gives it. */
73
+ function absoluteUrl(href) {
74
+ const url = safeUrl(href);
75
+ if (!url || typeof window === "undefined") return url;
76
+ return new URL(url, window.location.href).href;
77
+ }
70
78
  var MAX_UPLOAD_SIZE = 85899345920;
71
79
  function isUploadSizeExceeded(size) {
72
80
  return size > MAX_UPLOAD_SIZE;
@@ -110,12 +118,13 @@ function TreeFrame({ treeOnly, sidebarWidth, onWidthChange, children }) {
110
118
  children: [children, /* @__PURE__ */ jsx("div", { ...splitter.separatorProps })]
111
119
  });
112
120
  }
113
- function FileExplorer({ storages, onRefresh, refreshKey, onOpenFile, showUserHierarchy = false, groups = [], groupsLoading = false, selectedPath, onPathChange, getProviderAndClient, treeOnly = false, initialExpandedPaths, onExpandedPathsChange, objectUris, downloadCommand, extraRowActions, headerAccessory }) {
121
+ function FileExplorer({ storages, onRefresh, refreshKey, onOpenFile, showUserHierarchy = false, groups = [], groupsLoading = false, selectedPath, onPathChange, pathHref, getProviderAndClient, treeOnly = false, initialExpandedPaths, onExpandedPathsChange, objectUris, downloadCommand, extraRowActions, headerAccessory }) {
114
122
  const copySubmenu = useCopySubmenu();
115
123
  const t = useStrings().fileExplorer;
116
124
  const { common } = useStrings();
117
125
  const { copy } = useCopy();
118
126
  const slots = useSlots();
127
+ const { goTo } = useNavigation();
119
128
  const fileExplorerContext = useFileExplorer();
120
129
  const [listings, setListings] = useState(() => /* @__PURE__ */ new Map());
121
130
  const fetchesInFlightRef = useRef(/* @__PURE__ */ new Map());
@@ -146,7 +155,7 @@ function FileExplorer({ storages, onRefresh, refreshKey, onOpenFile, showUserHie
146
155
  if (treeMap[selectedPath]) return selectedPath;
147
156
  return `${selectedPath}/`;
148
157
  }, [selectedPath, treeMap]);
149
- const setSelectedPath = onPathChange;
158
+ const setSelectedPath = (path) => onPathChange ? onPathChange(path) : pathHref && goTo(pathHref(path));
150
159
  const [expandedPaths, setExpandedPaths] = useState(() => /* @__PURE__ */ new Set(["", ...initialExpandedPaths ?? []]));
151
160
  const onExpandedPathsChangeRef = useRef(onExpandedPathsChange);
152
161
  onExpandedPathsChangeRef.current = onExpandedPathsChange;
@@ -161,10 +170,12 @@ function FileExplorer({ storages, onRefresh, refreshKey, onOpenFile, showUserHie
161
170
  if (selectedNode?.type === "directory" && selectedNode.storageId) fetchNodeChildren(selectedNode, { replace: true });
162
171
  }, [refreshKey, selectedNode]);
163
172
  const [checkedItems, setCheckedItems] = useState(/* @__PURE__ */ new Set());
173
+ const [detailTab, setDetailTab] = useState("details");
164
174
  const [checkedItemsPath, setCheckedItemsPath] = useState(selectedPath);
165
175
  if (checkedItemsPath !== selectedPath) {
166
176
  setCheckedItemsPath(selectedPath);
167
177
  setCheckedItems(/* @__PURE__ */ new Set());
178
+ setDetailTab("details");
168
179
  }
169
180
  const selectedNodeChildren = useMemo(() => selectedNode ? getNodeChildren(parentToChildrenMap, selectedNode.path) : [], [parentToChildrenMap, selectedNode]);
170
181
  const findStorage = (storageId) => storages.find((s) => s.id === storageId);
@@ -244,7 +255,6 @@ function FileExplorer({ storages, onRefresh, refreshKey, onOpenFile, showUserHie
244
255
  const folderInputRef = useRef(null);
245
256
  const [deleteModalOpen, setDeleteModalOpen] = useState(false);
246
257
  const [pendingDelete, setPendingDelete] = useState(null);
247
- const [detailTab, setDetailTab] = useState("details");
248
258
  useEffect(() => clearPreviewCache, []);
249
259
  const { openMenu, contextMenu } = useContextMenu();
250
260
  const [sidebarWidth, setSidebarWidth] = usePersistedState(SIDEBAR_WIDTH_STORAGE_KEY, 300);
@@ -568,6 +578,9 @@ function FileExplorer({ storages, onRefresh, refreshKey, onOpenFile, showUserHie
568
578
  const closePreview = () => {
569
579
  setPreviewOpen(false);
570
580
  };
581
+ const openFromTree = (node) => {
582
+ if (treeOnly && node.type === "file") openPreview(node);
583
+ };
571
584
  const openUserNode = (node) => {
572
585
  handleSelectTreeItem(node.path, node);
573
586
  if (!expandedPaths.has(node.path)) handleToggleExpand(node.path);
@@ -651,9 +664,16 @@ function FileExplorer({ storages, onRefresh, refreshKey, onOpenFile, showUserHie
651
664
  destructive: true,
652
665
  onClick: () => deleteNode(node)
653
666
  });
654
- const lead = [{
667
+ const open = {
655
668
  label: isFile ? t.preview.viewDetails : t.preview.openFolder,
656
- icon: isFile ? /* @__PURE__ */ jsx(FileTextIcon, {}) : /* @__PURE__ */ jsx(FolderIcon, {}),
669
+ icon: isFile ? /* @__PURE__ */ jsx(FileTextIcon, {}) : /* @__PURE__ */ jsx(FolderIcon, {})
670
+ };
671
+ const href = pathHref?.(node.path);
672
+ const lead = [href ? {
673
+ ...open,
674
+ href
675
+ } : {
676
+ ...open,
657
677
  onClick: () => handleSelectTreeItem(node.path, node)
658
678
  }];
659
679
  const uriExtras = uris.map((value) => ({
@@ -661,6 +681,12 @@ function FileExplorer({ storages, onRefresh, refreshKey, onOpenFile, showUserHie
661
681
  value,
662
682
  icon: /* @__PURE__ */ jsx(LinkIcon, {})
663
683
  }));
684
+ const pageUrl = absoluteUrl(href);
685
+ if (pageUrl) uriExtras.unshift({
686
+ label: t.preview.link,
687
+ value: pageUrl,
688
+ icon: /* @__PURE__ */ jsx(LinkIcon, {})
689
+ });
664
690
  return assembleRowMenu({
665
691
  lead,
666
692
  actions,
@@ -705,10 +731,14 @@ function FileExplorer({ storages, onRefresh, refreshKey, onOpenFile, showUserHie
705
731
  "aria-current": "location",
706
732
  className: "truncate font-semibold text-(--theme-app)",
707
733
  children: label
734
+ }) : pathHref ? /* @__PURE__ */ jsx(AppLink, {
735
+ href: pathHref(node.path),
736
+ className: breadcrumbClass,
737
+ children: label
708
738
  }) : /* @__PURE__ */ jsx("button", {
709
739
  type: "button",
710
740
  onClick: () => handleSelectTreeItem(node.path, node),
711
- className: "max-w-[16rem] cursor-pointer truncate text-(--theme-muted-text-color) transition-colors hover:text-(--theme-app)",
741
+ className: breadcrumbClass,
712
742
  children: label
713
743
  })]
714
744
  }, node.path);
@@ -965,8 +995,10 @@ function FileExplorer({ storages, onRefresh, refreshKey, onOpenFile, showUserHie
965
995
  onToggleExpand: handleToggleExpand,
966
996
  onSelect: (path, node) => {
967
997
  handleSelectTreeItem(path, node);
968
- if (treeOnly && node.type === "file") openPreview(node);
998
+ openFromTree(node);
969
999
  },
1000
+ pathHref,
1001
+ onFollowLink: openFromTree,
970
1002
  onDropToFolder: onDropFiles,
971
1003
  onContextMenu: openNodeContextMenu,
972
1004
  onPreviewFile: openPreview,
@@ -1008,18 +1040,28 @@ function FileExplorer({ storages, onRefresh, refreshKey, onOpenFile, showUserHie
1008
1040
  children: t.chrome.storages
1009
1041
  }),
1010
1042
  /* @__PURE__ */ jsx(ListActionsHeader, { actionCount: 1 }),
1011
- rootNodes.map((rootNode) => /* @__PURE__ */ jsx(UserRow, {
1012
- node: rootNode,
1013
- icon: getNodeIcon(rootNode),
1014
- storageCount: getNodeChildren(parentToChildrenMap, rootNode.path).length,
1015
- getItems: () => [{
1043
+ rootNodes.map((rootNode) => {
1044
+ const href = pathHref?.(rootNode.path);
1045
+ const open = {
1016
1046
  label: t.preview.open,
1017
- icon: /* @__PURE__ */ jsx(FolderIcon, {}),
1018
- onClick: () => openUserNode(rootNode)
1019
- }],
1020
- openMenu,
1021
- onOpen: openUserNode
1022
- }, rootNode.path))
1047
+ icon: /* @__PURE__ */ jsx(FolderIcon, {})
1048
+ };
1049
+ return /* @__PURE__ */ jsx(UserRow, {
1050
+ node: rootNode,
1051
+ icon: getNodeIcon(rootNode),
1052
+ href,
1053
+ storageCount: getNodeChildren(parentToChildrenMap, rootNode.path).length,
1054
+ getItems: () => [href ? {
1055
+ ...open,
1056
+ href
1057
+ } : {
1058
+ ...open,
1059
+ onClick: () => openUserNode(rootNode)
1060
+ }],
1061
+ openMenu,
1062
+ onOpen: openUserNode
1063
+ }, rootNode.path);
1064
+ })
1023
1065
  ]
1024
1066
  }), rootNodes.length === 0 && /* @__PURE__ */ jsx(NoDataAvailablePreview, {
1025
1067
  selectedStorage: null,
@@ -1206,6 +1248,7 @@ function FileExplorer({ storages, onRefresh, refreshKey, onOpenFile, showUserHie
1206
1248
  onOpen: (n) => handleSelectTreeItem(n.path, n),
1207
1249
  onToggleCheck: handleItemCheckbox,
1208
1250
  onPreview: openPreview,
1251
+ pathHref,
1209
1252
  loadMore: loadMoreProps
1210
1253
  }, selectedNode.path) : /* @__PURE__ */ jsxs("div", {
1211
1254
  className: "h-full flex flex-col",
@@ -6,6 +6,7 @@ import { useStrings } from "../../components/Provider.js";
6
6
  import { Button } from "../../components/Button.js";
7
7
  import Markdown from "../../components/Markdown.js";
8
8
  import { formatBytes } from "../../format.js";
9
+ import { Editor } from "../../editor/Editor.js";
9
10
  import { getFileExtension } from "../lib/utils.js";
10
11
  import { getMonacoLanguage, previewLimitsFor } from "./previewType.js";
11
12
  import { CorsIssuePreview } from "../helpers.js";
@@ -14,9 +15,8 @@ import { countLines } from "./readCappedBytes.js";
14
15
  import { useObjectBlobUrl } from "./useObjectBlobUrl.js";
15
16
  import { useObjectText } from "./useObjectText.js";
16
17
  import { Fragment, jsx, jsxs } from "react/jsx-runtime";
17
- import { Suspense, lazy, useCallback, useMemo, useState } from "react";
18
+ import { useCallback, useMemo, useState } from "react";
18
19
  //#region src/file-explorer/FilePreview/Viewer.tsx
19
- var CodePreviewEditor = lazy(() => import("./CodePreviewEditor.js"));
20
20
  var PREVIEW_MARKDOWN_CONTROLS = {
21
21
  table: { fullscreen: false },
22
22
  mermaid: { fullscreen: false }
@@ -279,12 +279,16 @@ function CodeView({ node, url, onDownload, storage, onCorsRetry }) {
279
279
  controls: PREVIEW_MARKDOWN_CONTROLS,
280
280
  children: text ?? ""
281
281
  })
282
- }) : /* @__PURE__ */ jsx(Suspense, {
283
- fallback: /* @__PURE__ */ jsx(PreviewLoading, {}),
284
- children: /* @__PURE__ */ jsx(CodePreviewEditor, {
285
- value: text ?? "",
286
- language
287
- })
282
+ }) : /* @__PURE__ */ jsx(Editor, {
283
+ readOnly: true,
284
+ value: text ?? "",
285
+ language,
286
+ ariaLabel: node.name,
287
+ className: "!rounded-none !border-0",
288
+ fontSize: 12,
289
+ minimap: { enabled: false },
290
+ overviewRulerLanes: 0,
291
+ wordWrap: "off"
288
292
  })
289
293
  })
290
294
  })]
@@ -16,6 +16,8 @@ interface FileRowProps {
16
16
  rowIndex: number;
17
17
  /** The keyboard cursor row. Focus stays on the container, so nothing else marks it. */
18
18
  active: boolean;
19
+ /** Makes the row a link to the node. */
20
+ href?: string | undefined;
19
21
  checked: boolean;
20
22
  canPreview: boolean;
21
23
  getItems: () => MenuEntry[];
@@ -24,13 +26,15 @@ interface FileRowProps {
24
26
  onToggleCheck: (node: TreeNode, checked: boolean) => void;
25
27
  onPreview: (node: TreeNode) => void;
26
28
  }
27
- export declare function UserRow({ node, icon, storageCount, getItems, openMenu, onOpen, }: {
29
+ export declare function UserRow({ node, icon, href, storageCount, getItems, openMenu, onOpen, }: {
28
30
  node: TreeNode;
29
31
  icon: ReactNode;
32
+ /** Makes the row a link to the user. */
33
+ href?: string | undefined;
30
34
  storageCount: number;
31
35
  getItems: () => MenuEntry[];
32
36
  openMenu: OpenMenu;
33
37
  onOpen: (node: TreeNode) => void;
34
38
  }): import("react").JSX.Element;
35
- export declare function FileRow({ node, icon, rowId, rowIndex, active, checked, canPreview, getItems, openMenu, onOpen, onToggleCheck, onPreview, }: FileRowProps): import("react").JSX.Element;
39
+ export declare function FileRow({ node, icon, rowId, rowIndex, active, href, checked, canPreview, getItems, openMenu, onOpen, onToggleCheck, onPreview, }: FileRowProps): import("react").JSX.Element;
36
40
  export {};
@@ -2,10 +2,11 @@ import { cx } from "../cx.js";
2
2
  import { PreviewIcon } from "../icons.js";
3
3
  import { Tooltip } from "../components/Tooltip.js";
4
4
  import { useStrings } from "../components/Provider.js";
5
+ import { AppLink, isPlainClick } from "../components/AppLink.js";
5
6
  import { formatBytes } from "../format.js";
6
7
  import { RelativeTime } from "../components/relativeTime.js";
7
8
  import { Table } from "../components/Table.js";
8
- import { RowSelectCheckbox } from "../list/ListTable.js";
9
+ import { RowSelectCheckbox, STRETCHED_LINK } from "../list/ListTable.js";
9
10
  import { MoreButton, rowContextMenuProps, useRowMenuActive } from "../list/rowMenu.js";
10
11
  import { jsx, jsxs } from "react/jsx-runtime";
11
12
  import { DateTime } from "luxon";
@@ -20,16 +21,31 @@ function fileRowHeight(node) {
20
21
  function isInteractiveTarget(e) {
21
22
  return !!e.target.closest("a,button,input,label");
22
23
  }
23
- function UserRow({ node, icon, storageCount, getItems, openMenu, onOpen }) {
24
+ /** Opens the row on a click its controls don't take. Beside a link, which
25
+ * covers the row, that is only a click on a gap the link leaves, and a
26
+ * modified one there asks for a new tab, which only the link can give. */
27
+ function rowClickHandler(onOpen, href) {
28
+ return (e) => {
29
+ if (isInteractiveTarget(e) || href && !isPlainClick(e)) return;
30
+ onOpen();
31
+ };
32
+ }
33
+ /** A row's name: with an href, the link that covers the row, so the whole row
34
+ * opens in a new tab, shows its URL and reads as a link. */
35
+ function RowName({ name, href }) {
36
+ return href ? /* @__PURE__ */ jsx(AppLink, {
37
+ href,
38
+ "data-row-link": true,
39
+ className: STRETCHED_LINK,
40
+ children: name
41
+ }) : name;
42
+ }
43
+ function UserRow({ node, icon, href, storageCount, getItems, openMenu, onOpen }) {
24
44
  const t = useStrings().fileExplorer;
25
45
  const { active: menuActive, openMenu: openRowMenu } = useRowMenuActive(openMenu);
26
- const handleRowClick = (e) => {
27
- if (isInteractiveTarget(e)) return;
28
- onOpen(node);
29
- };
30
46
  return /* @__PURE__ */ jsxs("tr", {
31
- className: cx("group cursor-pointer", menuActive ? "theme-hover" : "hover:theme-hover"),
32
- onClick: handleRowClick,
47
+ className: cx("group cursor-pointer", menuActive ? "theme-hover" : "hover:theme-hover", href && "relative [&_:is(a,button,input,label,[role=checkbox]):not([data-row-link])]:relative [&_:is(a,button,input,label,[role=checkbox]):not([data-row-link])]:z-[1]"),
48
+ onClick: rowClickHandler(() => onOpen(node), href),
33
49
  ...rowContextMenuProps(openRowMenu, getItems),
34
50
  children: [
35
51
  /* @__PURE__ */ jsx(Table.Item, {
@@ -41,7 +57,10 @@ function UserRow({ node, icon, storageCount, getItems, openMenu, onOpen }) {
41
57
  children: icon
42
58
  }), /* @__PURE__ */ jsx("span", {
43
59
  className: "truncate text-[13px] font-medium text-(--theme-app)",
44
- children: node.displayName || node.name
60
+ children: /* @__PURE__ */ jsx(RowName, {
61
+ name: node.displayName || node.name,
62
+ href
63
+ })
45
64
  })]
46
65
  })
47
66
  }),
@@ -66,22 +85,18 @@ function UserRow({ node, icon, storageCount, getItems, openMenu, onOpen }) {
66
85
  ]
67
86
  });
68
87
  }
69
- function FileRow({ node, icon, rowId, rowIndex, active, checked, canPreview, getItems, openMenu, onOpen, onToggleCheck, onPreview }) {
88
+ function FileRow({ node, icon, rowId, rowIndex, active, href, checked, canPreview, getItems, openMenu, onOpen, onToggleCheck, onPreview }) {
70
89
  const t = useStrings().fileExplorer;
71
90
  const { active: menuActive, openMenu: openRowMenu } = useRowMenuActive(openMenu);
72
91
  const isDir = node.type === "directory";
73
92
  const modifiedDt = node.modified ? DateTime.fromISO(node.modified) : null;
74
93
  const typeLabel = node.root ? node.storageType ?? node.type : node.type;
75
- const handleRowClick = (e) => {
76
- if (isInteractiveTarget(e)) return;
77
- onOpen(node);
78
- };
79
94
  return /* @__PURE__ */ jsxs("tr", {
80
95
  id: rowId,
81
96
  "aria-rowindex": rowIndex,
82
97
  "aria-selected": checked,
83
- className: cx("group", checked ? "bg-(--theme-accent-subtle)/60" : menuActive ? "theme-hover" : "hover:theme-hover", active && "group-focus-visible/grid:ring-1 group-focus-visible/grid:ring-inset group-focus-visible/grid:ring-(--theme-link)", "cursor-pointer"),
84
- onClick: handleRowClick,
98
+ className: cx("group", checked ? "bg-(--theme-accent-subtle)/60" : menuActive ? "theme-hover" : "hover:theme-hover", active && "group-focus-visible/grid:ring-1 group-focus-visible/grid:ring-inset group-focus-visible/grid:ring-(--theme-link)", "cursor-pointer", href && "relative [&_:is(a,button,input,label,[role=checkbox]):not([data-row-link])]:relative [&_:is(a,button,input,label,[role=checkbox]):not([data-row-link])]:z-[1]"),
99
+ onClick: rowClickHandler(() => onOpen(node), href),
85
100
  style: { height: fileRowHeight(node) },
86
101
  ...rowContextMenuProps(openRowMenu, getItems),
87
102
  children: [
@@ -103,7 +118,10 @@ function FileRow({ node, icon, rowId, rowIndex, active, checked, canPreview, get
103
118
  className: "min-w-0",
104
119
  children: [/* @__PURE__ */ jsx("div", {
105
120
  className: "truncate text-[13px] font-medium leading-tight text-(--theme-app)",
106
- children: node.displayName || node.name
121
+ children: /* @__PURE__ */ jsx(RowName, {
122
+ name: node.displayName || node.name,
123
+ href
124
+ })
107
125
  }), isTwoLineRow(node) && /* @__PURE__ */ jsx("div", {
108
126
  className: "truncate text-xs leading-tight text-(--theme-muted-text-color)",
109
127
  children: node.contentType
@@ -30,8 +30,9 @@ interface FileRowsWindowProps {
30
30
  onOpen: (node: TreeNode) => void;
31
31
  onToggleCheck: (node: TreeNode, checked: boolean) => void;
32
32
  onPreview: (node: TreeNode) => void;
33
+ pathHref?: ((path: string) => string) | undefined;
33
34
  }
34
35
  /** Must stay a single child of `<Table>`: Table re-keys children by traversal
35
36
  * index, so a count that varied with scroll position would remount every row. */
36
- export declare function FileRowsWindow({ items, padTop, padBottom, nodes, checkedItems, activeIndex, rowIdPrefix, scrollEl, loadMore, getItems, openMenu, onOpen, onToggleCheck, onPreview, }: FileRowsWindowProps): import("react").JSX.Element;
37
+ export declare function FileRowsWindow({ items, padTop, padBottom, nodes, checkedItems, activeIndex, rowIdPrefix, scrollEl, loadMore, getItems, openMenu, onOpen, onToggleCheck, onPreview, pathHref, }: FileRowsWindowProps): import("react").JSX.Element;
37
38
  export {};
@@ -23,7 +23,7 @@ function spacerHeights(items, totalSize, scrollMargin) {
23
23
  }
24
24
  /** Must stay a single child of `<Table>`: Table re-keys children by traversal
25
25
  * index, so a count that varied with scroll position would remount every row. */
26
- function FileRowsWindow({ items, padTop, padBottom, nodes, checkedItems, activeIndex, rowIdPrefix, scrollEl, loadMore, getItems, openMenu, onOpen, onToggleCheck, onPreview }) {
26
+ function FileRowsWindow({ items, padTop, padBottom, nodes, checkedItems, activeIndex, rowIdPrefix, scrollEl, loadMore, getItems, openMenu, onOpen, onToggleCheck, onPreview, pathHref }) {
27
27
  useNestedScrollerWarning(scrollEl, nodes.length);
28
28
  useRowPitchAudit(scrollEl, nodes, items);
29
29
  return /* @__PURE__ */ jsxs(Fragment, { children: [
@@ -44,6 +44,7 @@ function FileRowsWindow({ items, padTop, padBottom, nodes, checkedItems, activeI
44
44
  rowId: `${rowIdPrefix}-${item.index}`,
45
45
  rowIndex: item.index + 2,
46
46
  active: item.index === activeIndex,
47
+ href: pathHref?.(node.path),
47
48
  node,
48
49
  icon,
49
50
  checked: checkedItems.has(node.path),
@@ -13,11 +13,13 @@ interface FileTableProps {
13
13
  onOpen: (node: TreeNode) => void;
14
14
  onToggleCheck: (node: TreeNode, checked: boolean) => void;
15
15
  onPreview: (node: TreeNode) => void;
16
+ /** The URL of a path; set, each row is a link to its node. */
17
+ pathHref?: ((path: string) => string) | undefined;
16
18
  /** Set when the folder has pages left to fetch. */
17
19
  loadMore?: LoadMoreState | undefined;
18
20
  }
19
21
  /** The file table. Give it a `key` of the folder path: a folder change must reset
20
22
  * the cursor, the scroll offset and the virtualizer's measurements, and a remount
21
23
  * does all three without a synchronising effect. */
22
- export declare function FileTable({ nodes, checkedItems, allSelected, someSelected, onSelectAll, getItems, openMenu, onOpen, onToggleCheck, onPreview, loadMore, }: FileTableProps): import("react").JSX.Element;
24
+ export declare function FileTable({ nodes, checkedItems, allSelected, someSelected, onSelectAll, getItems, openMenu, onOpen, onToggleCheck, onPreview, pathHref, loadMore, }: FileTableProps): import("react").JSX.Element;
23
25
  export {};