@peers-app/peers-ui 0.30.0 → 0.30.2

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.
@@ -4,10 +4,14 @@ interface ITabsProps {
4
4
  name: string;
5
5
  content: JSX.Element;
6
6
  active?: true;
7
+ /** Mount content on first activation, then preserve it while hidden. */
8
+ lazy?: boolean;
7
9
  }[];
8
10
  contentClassName?: string;
9
11
  activeTabName?: string;
10
12
  }
13
+ /** Switch between panels, eagerly mounting content unless a tab opts into lazy loading. */
11
14
  export declare const Tabs: (props: ITabsProps) => import("react/jsx-runtime").JSX.Element;
15
+ /** Constrain a screen's tab body to the available viewport with vertical scrolling. */
12
16
  export declare const ScreenTabBody: (props: React.PropsWithChildren) => import("react/jsx-runtime").JSX.Element;
13
17
  export {};
@@ -5,6 +5,7 @@ const jsx_runtime_1 = require("react/jsx-runtime");
5
5
  const react_1 = require("react");
6
6
  const globals_1 = require("../globals");
7
7
  let lastActive = "";
8
+ /** Switch between panels, eagerly mounting content unless a tab opts into lazy loading. */
8
9
  const Tabs = (props) => {
9
10
  const { tabs } = props;
10
11
  const [activeTab, _setActiveTab] = (0, react_1.useState)(() => {
@@ -29,10 +30,19 @@ const Tabs = (props) => {
29
30
  setActiveTab(tab);
30
31
  }, children: tab.name }) }, tab.name));
31
32
  }) }), (0, jsx_runtime_1.jsx)("div", { className: `tab-content ${props.contentClassName ?? "mt-3"}`, children: tabs.map((tab) => {
32
- return ((0, jsx_runtime_1.jsx)("div", { hidden: resolvedActiveTab.name !== tab.name, children: tab.content }, tab.name));
33
+ return ((0, jsx_runtime_1.jsx)(TabContent, { active: resolvedActiveTab.name === tab.name, lazy: tab.lazy, children: tab.content }, tab.name));
33
34
  }) })] }));
34
35
  };
35
36
  exports.Tabs = Tabs;
37
+ const TabContent = (props) => {
38
+ const [visited, setVisited] = (0, react_1.useState)(props.active);
39
+ (0, react_1.useEffect)(() => {
40
+ if (props.active)
41
+ setVisited(true);
42
+ }, [props.active]);
43
+ return ((0, jsx_runtime_1.jsx)("div", { hidden: !props.active, children: !props.lazy || props.active || visited ? props.children : null }));
44
+ };
45
+ /** Constrain a screen's tab body to the available viewport with vertical scrolling. */
36
46
  const ScreenTabBody = (props) => {
37
47
  return ((0, jsx_runtime_1.jsx)("div", { style: { maxHeight: "calc(100vh - 138px)", overflowY: "auto" }, children: props.children }));
38
48
  };
package/dist/globals.d.ts CHANGED
@@ -32,6 +32,9 @@ export declare const groups: import("@peers-app/peers-sdk").Observable<{
32
32
  iconClassName?: string | undefined;
33
33
  signedAt?: number | undefined;
34
34
  publicRole?: import("@peers-app/peers-sdk").GroupMemberRole | undefined;
35
+ fileRetention?: {
36
+ days: number | "never";
37
+ } | undefined;
35
38
  }[]>;
36
39
  /**
37
40
  * Start two-way hash synchronization after routes and persisted tabs are restored.
@@ -16,7 +16,8 @@ export interface IUseFileUrlResult {
16
16
  * ```
17
17
  *
18
18
  * Reads through `hostContract(SystemFiles)` in the screen's data context, the active group.
19
- * The URL is revoked when the file id changes or the component unmounts, so don't keep it.
19
+ * The URL is revoked when the file id changes or the component unmounts, so don't keep it. A
20
+ * file over `FILES_WHOLE_READ_MAX_BYTES` (64 MiB) gives an error: read a video, say, in ranges.
20
21
  *
21
22
  * @param fileId - The file's id; nothing is read while it's undefined.
22
23
  */
@@ -12,7 +12,8 @@ const react_1 = require("react");
12
12
  * ```
13
13
  *
14
14
  * Reads through `hostContract(SystemFiles)` in the screen's data context, the active group.
15
- * The URL is revoked when the file id changes or the component unmounts, so don't keep it.
15
+ * The URL is revoked when the file id changes or the component unmounts, so don't keep it. A
16
+ * file over `FILES_WHOLE_READ_MAX_BYTES` (64 MiB) gives an error: read a video, say, in ranges.
16
17
  *
17
18
  * @param fileId - The file's id; nothing is read while it's undefined.
18
19
  */
@@ -0,0 +1,5 @@
1
+ /** Inspect a context's files on demand; personal Settings can also show this device's storage. */
2
+ export declare const ContextFiles: (props: {
3
+ dataContextId: string;
4
+ showDeviceStorage?: boolean;
5
+ }) => import("react/jsx-runtime").JSX.Element;
@@ -0,0 +1,204 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.ContextFiles = void 0;
4
+ const jsx_runtime_1 = require("react/jsx-runtime");
5
+ const peers_sdk_1 = require("@peers-app/peers-sdk");
6
+ const react_1 = require("react");
7
+ const loading_indicator_1 = require("../../components/loading-indicator");
8
+ const DAY_MS = 86_400_000;
9
+ const stateLabels = {
10
+ referenced: "In use",
11
+ waiting: "Waiting to free",
12
+ expired: "Ready to free",
13
+ kept: "Kept",
14
+ never: "Never freed",
15
+ unindexed: "References not ready",
16
+ disabled: "Group disabled",
17
+ };
18
+ function errorMessage(error) {
19
+ const message = error instanceof Error ? error.message : String(error);
20
+ return message.includes("does not provide tool")
21
+ ? "This device's Peers needs updating to show files."
22
+ : message;
23
+ }
24
+ function count(value, noun) {
25
+ return `${value.toLocaleString()} ${noun}${value === 1 ? "" : "s"}`;
26
+ }
27
+ function size(bytes) {
28
+ const units = ["B", "KiB", "MiB", "GiB", "TiB"];
29
+ const power = bytes > 0 ? Math.min(Math.floor(Math.log(bytes) / Math.log(1024)), 4) : 0;
30
+ return `${(bytes / 1024 ** power).toLocaleString(undefined, { maximumFractionDigits: 1 })} ${units[power]}`;
31
+ }
32
+ /** Inspect a context's files on demand; personal Settings can also show this device's storage. */
33
+ const ContextFiles = (props) => {
34
+ const { dataContextId, showDeviceStorage = false } = props;
35
+ const [view, setView] = (0, react_1.useState)({ files: [], cursors: [], busy: true });
36
+ const [refresh, setRefresh] = (0, react_1.useState)(0);
37
+ const sessionRef = (0, react_1.useRef)();
38
+ async function loadPage(session, cursors = []) {
39
+ if (session.cancelled || !session.consumer || session.busy)
40
+ return;
41
+ const after = cursors.at(-1);
42
+ session.busy = true;
43
+ setView((previous) => ({ ...previous, busy: true, error: undefined }));
44
+ try {
45
+ const page = await session.consumer.tools.listFiles({
46
+ limit: 50,
47
+ ...(after ? { after } : {}),
48
+ });
49
+ if (session.cancelled)
50
+ return;
51
+ setView((previous) => session.cancelled
52
+ ? previous
53
+ : {
54
+ ...previous,
55
+ ...page,
56
+ cursors,
57
+ nextCursor: page.nextCursor,
58
+ busy: false,
59
+ });
60
+ }
61
+ catch (error) {
62
+ if (!session.cancelled) {
63
+ setView((previous) => ({ ...previous, busy: false, error: errorMessage(error) }));
64
+ }
65
+ }
66
+ finally {
67
+ session.busy = false;
68
+ }
69
+ }
70
+ (0, react_1.useEffect)(() => {
71
+ const session = { cancelled: false, busy: false };
72
+ const controller = new AbortController();
73
+ sessionRef.current = session;
74
+ setView({ files: [], cursors: [], busy: true });
75
+ /** Returns whether the group is disabled; a failed read doesn't stop the list. */
76
+ async function readSettings() {
77
+ try {
78
+ const user = await (0, peers_sdk_1.getUserContext)();
79
+ const group = dataContextId === user.userId
80
+ ? undefined
81
+ : await (0, peers_sdk_1.Groups)(user.userDataContext).get(dataContextId);
82
+ if (!session.cancelled) {
83
+ setView((previous) => ({
84
+ ...previous,
85
+ defaultRetention: !group?.fileRetention,
86
+ disabled: !!group?.disabled,
87
+ }));
88
+ }
89
+ return !!group?.disabled;
90
+ }
91
+ catch (error) {
92
+ if (!session.cancelled) {
93
+ setView((previous) => ({ ...previous, settingsError: errorMessage(error) }));
94
+ }
95
+ return false;
96
+ }
97
+ }
98
+ async function readStatus(consumer) {
99
+ try {
100
+ const status = await consumer.tools.getStatus();
101
+ if (!session.cancelled)
102
+ setView((previous) => ({ ...previous, status }));
103
+ }
104
+ catch (error) {
105
+ if (!session.cancelled) {
106
+ setView((previous) => ({ ...previous, statusError: errorMessage(error) }));
107
+ }
108
+ }
109
+ }
110
+ void (async () => {
111
+ try {
112
+ let transport = (0, peers_sdk_1.getHostContractTransport)();
113
+ if (!transport) {
114
+ try {
115
+ transport = await (0, peers_sdk_1.waitForHostContractTransport)({
116
+ timeoutMs: 3_000,
117
+ signal: controller.signal,
118
+ });
119
+ }
120
+ catch {
121
+ throw new Error("File storage isn't available on this device right now.");
122
+ }
123
+ }
124
+ if (session.cancelled)
125
+ return;
126
+ const consumer = (0, peers_sdk_1.createContractConsumer)((0, peers_sdk_1.getFileStorageContractDefinition)(), transport, {
127
+ dataContextId,
128
+ });
129
+ session.consumer = consumer;
130
+ await Promise.all([
131
+ // The host refuses a disabled group's context, so don't ask it to list one.
132
+ readSettings().then((disabled) => {
133
+ if (!disabled)
134
+ return loadPage(session);
135
+ if (!session.cancelled)
136
+ setView((previous) => ({ ...previous, busy: false }));
137
+ }),
138
+ showDeviceStorage ? readStatus(consumer) : undefined,
139
+ ]);
140
+ }
141
+ catch (error) {
142
+ if (!session.cancelled) {
143
+ setView((previous) => ({ ...previous, busy: false, error: errorMessage(error) }));
144
+ }
145
+ }
146
+ })();
147
+ return () => {
148
+ session.cancelled = true;
149
+ controller.abort();
150
+ session.consumer?.dispose();
151
+ };
152
+ }, [dataContextId, showDeviceStorage, refresh]);
153
+ return ((0, jsx_runtime_1.jsxs)("section", { "aria-label": "Files", children: [(0, jsx_runtime_1.jsxs)("div", { className: "d-flex align-items-center justify-content-between mb-3", children: [(0, jsx_runtime_1.jsx)("h5", { className: "mb-0", children: "Files" }), (0, jsx_runtime_1.jsx)("button", { type: "button", className: "btn btn-outline-secondary btn-sm", onClick: () => setRefresh((value) => value + 1), children: "Refresh" })] }), view.error && (0, jsx_runtime_1.jsx)("p", { role: "alert", children: view.error }), view.summary && ((0, jsx_runtime_1.jsx)(FilesSummary, { summary: view.summary, defaultRetention: view.defaultRetention })), view.disabled && ((0, jsx_runtime_1.jsx)("p", { className: "alert alert-secondary", children: "This group is disabled, so its files are kept on this device. They can be listed once it's enabled." })), view.settingsError && (0, jsx_runtime_1.jsxs)("p", { role: "alert", children: ["Couldn't read group settings: ", view.settingsError] }), showDeviceStorage && (0, jsx_runtime_1.jsx)(DeviceStorage, { status: view.status, error: view.statusError }), (0, jsx_runtime_1.jsx)(FileList, { view: view, onMove: (cursors) => {
154
+ const session = sessionRef.current;
155
+ if (session)
156
+ void loadPage(session, cursors);
157
+ } }), view.busy && (0, jsx_runtime_1.jsx)(loading_indicator_1.LoadingIndicator, {})] }));
158
+ };
159
+ exports.ContextFiles = ContextFiles;
160
+ const FilesSummary = (props) => {
161
+ const { summary } = props;
162
+ const retention = summary.retentionMs === "never"
163
+ ? "Never: files here aren't freed"
164
+ : `${count(summary.retentionMs / DAY_MS, "day")}${props.defaultRetention && summary.retentionMs === peers_sdk_1.FILE_RETENTION_MS ? " (the default)" : ""}`;
165
+ return ((0, jsx_runtime_1.jsxs)("div", { className: "mb-3", children: [(0, jsx_runtime_1.jsxs)("p", { className: "mb-1", children: [count(summary.files, "file"), " \u00B7 ", size(summary.bytes), " total file size"] }), (0, jsx_runtime_1.jsx)("p", { className: "small text-muted mb-1", children: "Shared contents count for each file; this isn't disk use." }), (0, jsx_runtime_1.jsx)("ul", { className: "list-inline mb-2", children: Object.entries(summary.states)
166
+ .filter(([, count]) => count > 0)
167
+ .map(([state, count]) => ((0, jsx_runtime_1.jsxs)("li", { className: "list-inline-item", children: [stateLabels[state], ": ", count.toLocaleString()] }, state))) }), (0, jsx_runtime_1.jsxs)("p", { className: "mb-1", children: ["Retention: ", retention] }), (0, jsx_runtime_1.jsxs)("p", { className: "small text-muted mb-0", children: ["Package bundles are freed after 7 unused days, whatever the retention. Retention can't be changed yet.", summary.retentionMs !== "never" && " Other unused files are freed 7 days after it ends."] })] }));
168
+ };
169
+ const FileList = (props) => {
170
+ const { files, cursors, summary, busy, nextCursor } = props.view;
171
+ if (!summary)
172
+ return null;
173
+ if (summary.files === 0)
174
+ return (0, jsx_runtime_1.jsx)("p", { children: "No files here yet." });
175
+ return ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [(0, jsx_runtime_1.jsx)("p", { className: "small text-muted", children: "Contents and deadlines below are on this device. Shared or recently saved contents may stay longer." }), (cursors.length > 0 || nextCursor) && ((0, jsx_runtime_1.jsxs)("nav", { "aria-label": "File pages", className: "d-flex flex-wrap align-items-center gap-2 mb-2", children: [(0, jsx_runtime_1.jsx)("button", { type: "button", className: "btn btn-outline-secondary btn-sm", disabled: busy || cursors.length === 0, onClick: () => props.onMove(cursors.slice(0, -1)), children: "Previous" }), (0, jsx_runtime_1.jsxs)("span", { className: "small", children: ["Page ", (cursors.length + 1).toLocaleString()] }), (0, jsx_runtime_1.jsx)("button", { type: "button", className: "btn btn-outline-secondary btn-sm", disabled: busy || !nextCursor, onClick: () => nextCursor && props.onMove([...cursors, nextCursor]), children: "Next" })] })), files.length === 0 && (0, jsx_runtime_1.jsx)("p", { children: "No files on this page. Use Previous or Refresh to see files." }), (0, jsx_runtime_1.jsx)("ul", { className: "list-group", children: files.map((file) => ((0, jsx_runtime_1.jsx)(FileRow, { file: file }, file.fileId))) })] }));
176
+ };
177
+ const FileRow = ({ file }) => ((0, jsx_runtime_1.jsxs)("li", { className: "list-group-item text-break", children: [(0, jsx_runtime_1.jsx)("div", { className: "fw-semibold", children: file.name || "Unnamed file" }), (0, jsx_runtime_1.jsx)(FileFlags, { file: file }), (0, jsx_runtime_1.jsxs)("div", { className: "small", children: [size(file.fileSize), " \u00B7 ", file.mimeType || "Unknown file type"] }), (0, jsx_runtime_1.jsxs)("div", { className: "small", children: [count(file.referenceCount, "reference"), file.referenceTables.length > 0 ? ` in ${file.referenceTables.join(", ")}` : ""] }), (0, jsx_runtime_1.jsxs)("div", { className: "small", children: ["Contents on this device: ", { all: "All", some: "Some", none: "None" }[file.chunks]] }), (0, jsx_runtime_1.jsx)("div", { className: "small", children: (0, jsx_runtime_1.jsx)(FileState, { file: file }) }), (0, jsx_runtime_1.jsx)("small", { className: "text-muted", children: file.fileId })] }));
178
+ const FileFlags = ({ file }) => ((0, jsx_runtime_1.jsx)("div", { className: "d-flex flex-wrap gap-1", children: file.runtimeBundle ? ((0, jsx_runtime_1.jsx)("span", { className: "badge text-bg-secondary text-wrap", children: "Package bundle: freed 7 days after no package version uses it" })) : ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [file.legacy && ((0, jsx_runtime_1.jsx)("span", { className: "badge text-bg-warning text-wrap", children: "Legacy: stays until explicit delete is available" })), file.kept && ((0, jsx_runtime_1.jsx)("span", { className: "badge text-bg-warning text-wrap", children: "Kept: never freed automatically" }))] })) }));
179
+ const FileState = ({ file }) => {
180
+ if (file.state === "referenced")
181
+ return (0, jsx_runtime_1.jsx)(jsx_runtime_1.Fragment, { children: "In use" });
182
+ if (file.state === "expired")
183
+ return (0, jsx_runtime_1.jsx)(jsx_runtime_1.Fragment, { children: "Can be freed at the next collection on this device" });
184
+ if (file.state === "waiting") {
185
+ return ((0, jsx_runtime_1.jsx)(jsx_runtime_1.Fragment, { children: file.expiresAt !== undefined
186
+ ? `Can be freed on this device from ${new Date(file.expiresAt).toLocaleString()}`
187
+ : `Waiting on this device: ${file.retainedReason || "retention hasn't started yet"}` }));
188
+ }
189
+ // The badges and the summary already say why a file is kept; only missing references need detail.
190
+ return ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [stateLabels[file.state], file.state === "unindexed" && file.retainedReason ? `: ${file.retainedReason}` : ""] }));
191
+ };
192
+ const DeviceStorage = (props) => {
193
+ const { status, error } = props;
194
+ return ((0, jsx_runtime_1.jsxs)("section", { "aria-label": "This device's storage", className: "border rounded p-3 mb-3", children: [(0, jsx_runtime_1.jsx)("h6", { children: "This device's storage" }), error && (0, jsx_runtime_1.jsx)("p", { role: "alert", children: error }), status && ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [(0, jsx_runtime_1.jsxs)("p", { className: "mb-1", children: [count(status.store.count, "chunk"), " \u00B7 ", size(status.store.bytes), " stored across your personal space and groups"] }), (0, jsx_runtime_1.jsx)(CollectionStatus, { status: status })] }))] }));
195
+ };
196
+ const CollectionStatus = ({ status }) => {
197
+ const collection = status.collection;
198
+ const last = collection?.lastRun;
199
+ return ((0, jsx_runtime_1.jsxs)("div", { className: "small text-muted", children: [collection?.state === "running" && (0, jsx_runtime_1.jsx)("div", { children: "Collection is running." }), last ? ((0, jsx_runtime_1.jsxs)("div", { children: ["Last collection: ", new Date(last.finishedAt).toLocaleString(), " \u2014", " ", last.state === "skipped"
200
+ ? `skipped: ${last.reason || "not ready"}`
201
+ : `${last.dryRun ? "preview: would free" : "freed"} ${size(last.bytesFreed)} in ${count(last.chunksFreed, "chunk")}${last.state === "stopped" ? `; stopped: ${last.reason || "interrupted"}` : ""}`, last.problems.length > 0 && ` (${last.problems.join("; ")})`] })) : ((0, jsx_runtime_1.jsx)("div", { children: "No collection has run this session." })), (0, jsx_runtime_1.jsxs)("div", { children: ["Next collection:", " ", collection?.nextRunAt !== undefined
202
+ ? new Date(collection.nextRunAt).toLocaleString()
203
+ : "not scheduled"] })] }));
204
+ };
@@ -1,6 +1,6 @@
1
1
  interface IProps {
2
2
  groupId: string;
3
3
  }
4
- /** Edit group metadata and manage members and invitations inside Identity. */
4
+ /** Edit group metadata, manage members and invitations, and inspect files inside Identity. */
5
5
  export declare const GroupDetails: (props: IProps) => import("react/jsx-runtime").JSX.Element;
6
6
  export {};
@@ -10,8 +10,9 @@ const editor_inline_1 = require("../../components/markdown-editor/editor-inline"
10
10
  const save_button_1 = require("../../components/save-button");
11
11
  const tabs_1 = require("../../components/tabs");
12
12
  const hooks_1 = require("../../hooks");
13
+ const context_files_1 = require("../files/context-files");
13
14
  const group_members_1 = require("./group-members");
14
- /** Edit group metadata and manage members and invitations inside Identity. */
15
+ /** Edit group metadata, manage members and invitations, and inspect files inside Identity. */
15
16
  const GroupDetails = (props) => {
16
17
  const group = (0, hooks_1.usePromise)(async () => {
17
18
  const userContext = await (0, peers_sdk_1.getUserContext)();
@@ -53,6 +54,11 @@ const GroupDetails = (props) => {
53
54
  name: "Members",
54
55
  content: ((0, jsx_runtime_1.jsx)("div", { children: (0, jsx_runtime_1.jsx)(GroupMembersWrapper, { groupId: props.groupId }) })),
55
56
  },
57
+ {
58
+ name: "Files",
59
+ lazy: true,
60
+ content: (0, jsx_runtime_1.jsx)(context_files_1.ContextFiles, { dataContextId: props.groupId }),
61
+ },
56
62
  ] }, group.groupId)] }));
57
63
  };
58
64
  exports.GroupDetails = GroupDetails;
@@ -45,6 +45,7 @@ const globals = __importStar(require("../../globals"));
45
45
  const hooks_1 = require("../../hooks");
46
46
  const layout_picker_1 = require("../../root-layout/layout-picker");
47
47
  const identity_ipc_1 = require("../device-pairing/identity-ipc");
48
+ const context_files_1 = require("../files/context-files");
48
49
  const identity_routes_1 = require("../identity/identity-routes");
49
50
  const network_viewer_ipc_1 = require("../network-viewer/network-viewer-ipc");
50
51
  const color_mode_dropdown_1 = require("./color-mode-dropdown");
@@ -74,9 +75,17 @@ const SettingsPage = () => {
74
75
  name: "Advanced",
75
76
  content: (0, jsx_runtime_1.jsx)(AdvancedSettingsTab, {}),
76
77
  },
78
+ {
79
+ name: "Files",
80
+ lazy: true,
81
+ content: (0, jsx_runtime_1.jsx)(PersonalFilesTab, {}),
82
+ },
77
83
  ] }) }), (0, jsx_runtime_1.jsxs)("footer", { className: "flex-shrink-0 border-top px-3 py-2 small text-muted", children: ["Peers ", getAppVersion()] })] }));
78
84
  };
79
85
  exports.SettingsPage = SettingsPage;
86
+ const PersonalFilesTab = () => {
87
+ return (0, jsx_runtime_1.jsx)(context_files_1.ContextFiles, { dataContextId: globals.me.userId, showDeviceStorage: true });
88
+ };
80
89
  const UserSettingsTab = () => {
81
90
  return ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [(0, jsx_runtime_1.jsx)(ProfileSection, {}), (0, jsx_runtime_1.jsx)(IdentityAccountSection, {}), (0, jsx_runtime_1.jsx)(AddDeviceSection, {}), (0, jsx_runtime_1.jsx)(LogoutSection, {})] }));
82
91
  };
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * Read a package's UI or routes bundle as text, through System Files in the context its
3
- * version is installed in.
3
+ * version is installed in. A bundle is evaluated whole, so it may be as large as any file.
4
4
  *
5
5
  * @param dataContextId - Context of the selected package version.
6
6
  * @param fileId - The bundle's file id.
@@ -16,13 +16,15 @@ function filesIn(dataContextId) {
16
16
  const decoder = new TextDecoder("utf-8", { ignoreBOM: true });
17
17
  /**
18
18
  * Read a package's UI or routes bundle as text, through System Files in the context its
19
- * version is installed in.
19
+ * version is installed in. A bundle is evaluated whole, so it may be as large as any file.
20
20
  *
21
21
  * @param dataContextId - Context of the selected package version.
22
22
  * @param fileId - The bundle's file id.
23
23
  */
24
24
  async function readBundleText(dataContextId, fileId) {
25
25
  const files = filesIn(dataContextId);
26
- const { bytes } = await (0, peers_sdk_1.readWholeFile)((input) => files.tools.read(input), fileId);
26
+ const { bytes } = await (0, peers_sdk_1.readWholeFile)((input) => files.tools.read(input), fileId, {
27
+ maxBytes: peers_sdk_1.FILES_MAX_FILE_BYTES,
28
+ });
27
29
  return decoder.decode(bytes);
28
30
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@peers-app/peers-ui",
3
- "version": "0.30.0",
3
+ "version": "0.30.2",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "git+https://github.com/peers-app/peers-ui.git"
@@ -36,7 +36,7 @@
36
36
  "lint:fix": "biome check --write ."
37
37
  },
38
38
  "peerDependencies": {
39
- "@peers-app/peers-sdk": "^0.30.0",
39
+ "@peers-app/peers-sdk": "^0.30.2",
40
40
  "bootstrap": "^5.3.3",
41
41
  "react": "^18.0.0",
42
42
  "react-dom": "^18.0.0"
@@ -47,7 +47,7 @@
47
47
  "@babel/preset-react": "^7.24.1",
48
48
  "@babel/preset-typescript": "^7.27.1",
49
49
  "@biomejs/biome": "2.3.11",
50
- "@peers-app/peers-sdk": "0.30.0",
50
+ "@peers-app/peers-sdk": "0.30.2",
51
51
  "@testing-library/dom": "^10.4.0",
52
52
  "@testing-library/jest-dom": "^6.6.3",
53
53
  "@testing-library/react": "^16.3.0",