canopycms 0.1.0 → 0.2.0-int.121
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/CHANGELOG.md +202 -43
- package/dist/api/assets.d.ts +27 -7
- package/dist/api/assets.js +118 -47
- package/dist/api/client.d.ts +5 -3
- package/dist/api/client.js +7 -3
- package/dist/api/index.d.ts +1 -1
- package/dist/assets/asset-prefixes.d.ts +4 -1
- package/dist/assets/asset-prefixes.js +3 -1
- package/dist/assets/editor-thumbnail-widths.d.ts +11 -0
- package/dist/assets/editor-thumbnail-widths.js +7 -0
- package/dist/assets/finalize.d.ts +7 -6
- package/dist/assets/finalize.js +29 -10
- package/dist/assets/keys.d.ts +4 -1
- package/dist/assets/keys.js +4 -1
- package/dist/assets/materialize.d.ts +17 -2
- package/dist/assets/materialize.js +57 -4
- package/dist/assets/store-local.d.ts +18 -9
- package/dist/assets/store-local.js +58 -26
- package/dist/assets/store-s3.d.ts +25 -6
- package/dist/assets/store-s3.js +81 -19
- package/dist/assets/types.d.ts +33 -4
- package/dist/cli/cli.js +151 -56
- package/dist/config/types.d.ts +3 -2
- package/dist/editor/BranchManager.js +2 -1
- package/dist/editor/CommentsPanel.js +67 -29
- package/dist/editor/Editor.d.ts +3 -0
- package/dist/editor/Editor.js +93 -39
- package/dist/editor/EntryNavigator.d.ts +17 -5
- package/dist/editor/EntryNavigator.js +66 -31
- package/dist/editor/FormHeader.d.ts +2 -1
- package/dist/editor/FormHeader.js +9 -5
- package/dist/editor/FormRenderer.d.ts +7 -2
- package/dist/editor/FormRenderer.js +51 -16
- package/dist/editor/PreviewFrame.d.ts +13 -2
- package/dist/editor/PreviewFrame.js +62 -5
- package/dist/editor/PreviewPane.d.ts +17 -1
- package/dist/editor/PreviewPane.js +55 -8
- package/dist/editor/PreviewToolbarTools.d.ts +8 -0
- package/dist/editor/PreviewToolbarTools.js +9 -1
- package/dist/editor/branch-status.d.ts +6 -0
- package/dist/editor/branch-status.js +10 -0
- package/dist/editor/comments/BranchComments.d.ts +2 -4
- package/dist/editor/comments/BranchComments.js +7 -11
- package/dist/editor/comments/ThreadCarousel.d.ts +2 -8
- package/dist/editor/comments/ThreadCarousel.js +4 -49
- package/dist/editor/comments/ThreadReplyControls.js +5 -2
- package/dist/editor/components/BranchLockBanner.d.ts +8 -7
- package/dist/editor/components/BranchLockBanner.js +30 -3
- package/dist/editor/components/BranchPill.d.ts +5 -4
- package/dist/editor/components/BranchPill.js +6 -8
- package/dist/editor/components/BranchRowLock.d.ts +12 -0
- package/dist/editor/components/BranchRowLock.js +19 -0
- package/dist/editor/components/BranchesDrawer.js +3 -3
- package/dist/editor/components/EditorHeader.d.ts +2 -6
- package/dist/editor/components/EditorHeader.js +14 -28
- package/dist/editor/components/EditorRail.d.ts +3 -4
- package/dist/editor/components/EditorRail.js +3 -4
- package/dist/editor/components/EntryBreadcrumb.d.ts +9 -10
- package/dist/editor/components/EntryBreadcrumb.js +8 -11
- package/dist/editor/components/PreviewLinkToast.d.ts +5 -0
- package/dist/editor/components/PreviewLinkToast.js +33 -0
- package/dist/editor/components/QuickOpenPalette.d.ts +7 -1
- package/dist/editor/components/QuickOpenPalette.js +6 -1
- package/dist/editor/components/RenameEntryModal.d.ts +6 -1
- package/dist/editor/components/RenameEntryModal.js +20 -5
- package/dist/editor/components/SaveSplitButton.d.ts +3 -2
- package/dist/editor/components/SaveSplitButton.js +25 -12
- package/dist/editor/components/StagedChangesDrawer.d.ts +0 -1
- package/dist/editor/components/StagedChangesDrawer.js +5 -4
- package/dist/editor/components/UndoToastMessage.d.ts +6 -5
- package/dist/editor/components/UndoToastMessage.js +6 -4
- package/dist/editor/components/UnsavedMark.d.ts +9 -0
- package/dist/editor/components/UnsavedMark.js +20 -0
- package/dist/editor/components/UserBadge.js +13 -11
- package/dist/editor/copy.d.ts +4 -0
- package/dist/editor/copy.js +4 -0
- package/dist/editor/editor-utils.d.ts +49 -9
- package/dist/editor/editor-utils.js +117 -16
- package/dist/editor/entry-search.d.ts +4 -2
- package/dist/editor/entry-search.js +2 -3
- package/dist/editor/fields/FieldLabel.d.ts +1 -1
- package/dist/editor/fields/FieldLabel.js +1 -1
- package/dist/editor/fields/ImageField.d.ts +1 -1
- package/dist/editor/fields/ImageField.js +36 -27
- package/dist/editor/fields/ObjectField.d.ts +13 -2
- package/dist/editor/fields/ObjectField.js +38 -9
- package/dist/editor/fields/entry-link/EntryLinkContext.d.ts +2 -0
- package/dist/editor/fields/entry-link/InsertEntryLink.js +3 -1
- package/dist/editor/group-manager/index.js +1 -1
- package/dist/editor/hooks/useBranchManager.d.ts +2 -0
- package/dist/editor/hooks/useBranchManager.js +1 -0
- package/dist/editor/hooks/useEditorLayout.d.ts +4 -0
- package/dist/editor/hooks/useEditorLayout.js +5 -0
- package/dist/editor/hooks/useEntryManager.d.ts +2 -0
- package/dist/editor/hooks/useEntryManager.js +10 -8
- package/dist/editor/media/AssetCard.d.ts +6 -3
- package/dist/editor/media/AssetCard.js +24 -9
- package/dist/editor/media/AssetListRow.d.ts +9 -4
- package/dist/editor/media/AssetListRow.js +18 -9
- package/dist/editor/media/CropStep.d.ts +8 -10
- package/dist/editor/media/CropStep.js +39 -27
- package/dist/editor/media/MediaLibrary.js +5 -4
- package/dist/editor/media/MediaLibraryBody.d.ts +1 -1
- package/dist/editor/media/MediaLibraryBody.js +65 -37
- package/dist/editor/media/crop-availability.d.ts +6 -0
- package/dist/editor/media/crop-availability.js +19 -0
- package/dist/editor/media/crop-math.d.ts +19 -13
- package/dist/editor/media/crop-math.js +50 -11
- package/dist/editor/media/useAssetLibrary.d.ts +6 -3
- package/dist/editor/media/useAssetLibrary.js +17 -10
- package/dist/editor/pane-header.d.ts +6 -0
- package/dist/editor/pane-header.js +6 -0
- package/dist/editor/permission-manager/index.js +1 -1
- package/dist/editor/preview-bridge.d.ts +23 -0
- package/dist/editor/preview-bridge.js +105 -4
- package/dist/editor/preview-path.d.ts +16 -4
- package/dist/editor/preview-path.js +80 -19
- package/dist/editor/shell/ActivityRail.d.ts +1 -3
- package/dist/editor/shell/ActivityRail.js +3 -3
- package/dist/editor/shell/EditorShell.d.ts +15 -0
- package/dist/editor/shell/EditorShell.js +17 -1
- package/dist/editor/shell/OverlayHeader.d.ts +19 -0
- package/dist/editor/shell/OverlayHeader.js +15 -0
- package/dist/editor/shell/PaneLayout.d.ts +6 -1
- package/dist/editor/shell/PaneLayout.js +68 -13
- package/dist/editor/shell/SidePanelHost.d.ts +8 -6
- package/dist/editor/shell/SidePanelHost.js +70 -33
- package/dist/editor/shell/TopBar.js +2 -1
- package/dist/editor/theme.d.ts +1 -0
- package/dist/editor/theme.js +19 -1
- package/dist/editor/undo-removal.d.ts +19 -10
- package/dist/editor/undo-removal.js +44 -16
- package/dist/server.d.ts +2 -0
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +2 -2
package/dist/cli/cli.js
CHANGED
|
@@ -13321,6 +13321,21 @@ var init_generate_ai_content = __esm({
|
|
|
13321
13321
|
}
|
|
13322
13322
|
});
|
|
13323
13323
|
|
|
13324
|
+
// dist/utils/logger.js
|
|
13325
|
+
function canopyLogWarn3(...args) {
|
|
13326
|
+
active3.warn(...args);
|
|
13327
|
+
}
|
|
13328
|
+
function canopyLogError2(...args) {
|
|
13329
|
+
active3.error(...args);
|
|
13330
|
+
}
|
|
13331
|
+
var active3;
|
|
13332
|
+
var init_logger3 = __esm({
|
|
13333
|
+
"dist/utils/logger.js"() {
|
|
13334
|
+
"use strict";
|
|
13335
|
+
active3 = console;
|
|
13336
|
+
}
|
|
13337
|
+
});
|
|
13338
|
+
|
|
13324
13339
|
// dist/assets/asset-prefixes.js
|
|
13325
13340
|
var ASSET_PREFIXES;
|
|
13326
13341
|
var init_asset_prefixes = __esm({
|
|
@@ -13330,6 +13345,8 @@ var init_asset_prefixes = __esm({
|
|
|
13330
13345
|
originals: "asset-originals",
|
|
13331
13346
|
staging: "asset-staging",
|
|
13332
13347
|
meta: "asset-meta",
|
|
13348
|
+
/** One marker per file removed from the media library; its meta stays (see `hideAsset`). */
|
|
13349
|
+
hidden: "asset-hidden",
|
|
13333
13350
|
public: "assets",
|
|
13334
13351
|
/** Transform-output prefix; directive-aware key building lives in transform-directives.ts. */
|
|
13335
13352
|
transform: "assets/t"
|
|
@@ -13337,21 +13354,6 @@ var init_asset_prefixes = __esm({
|
|
|
13337
13354
|
}
|
|
13338
13355
|
});
|
|
13339
13356
|
|
|
13340
|
-
// dist/utils/logger.js
|
|
13341
|
-
function canopyLogWarn3(...args) {
|
|
13342
|
-
active3.warn(...args);
|
|
13343
|
-
}
|
|
13344
|
-
function canopyLogError2(...args) {
|
|
13345
|
-
active3.error(...args);
|
|
13346
|
-
}
|
|
13347
|
-
var active3;
|
|
13348
|
-
var init_logger3 = __esm({
|
|
13349
|
-
"dist/utils/logger.js"() {
|
|
13350
|
-
"use strict";
|
|
13351
|
-
active3 = console;
|
|
13352
|
-
}
|
|
13353
|
-
});
|
|
13354
|
-
|
|
13355
13357
|
// dist/assets/sharp-loader.js
|
|
13356
13358
|
function loadSharp() {
|
|
13357
13359
|
if (!loading) {
|
|
@@ -13721,7 +13723,11 @@ var init_transform = __esm({
|
|
|
13721
13723
|
|
|
13722
13724
|
// dist/assets/materialize.js
|
|
13723
13725
|
async function storeTransform(store, parsed, key, options = {}) {
|
|
13724
|
-
const
|
|
13726
|
+
const { source } = options;
|
|
13727
|
+
if (source && source.meta.hash32 !== parsed.hash32) {
|
|
13728
|
+
throw new Error(`source is asset ${source.meta.hash32}, not ${parsed.hash32}`);
|
|
13729
|
+
}
|
|
13730
|
+
const meta = source?.meta ?? await store.getMeta(parsed.hash32);
|
|
13725
13731
|
if (!meta) {
|
|
13726
13732
|
return { ok: false, status: 404, error: `Asset ${parsed.hash32} has no meta in the store` };
|
|
13727
13733
|
}
|
|
@@ -13739,7 +13745,7 @@ async function storeTransform(store, parsed, key, options = {}) {
|
|
|
13739
13745
|
if (requestedFormat === void 0 && parsed.ext !== meta.ext) {
|
|
13740
13746
|
return { ok: false, status: 400, error: "Extension does not match the source format" };
|
|
13741
13747
|
}
|
|
13742
|
-
const original = await store.readOriginal(parsed.hash32, meta.ext);
|
|
13748
|
+
const original = source ? { data: source.data, ext: meta.ext } : await store.readOriginal(parsed.hash32, meta.ext);
|
|
13743
13749
|
if (!original) {
|
|
13744
13750
|
return { ok: false, status: 404, error: `Asset ${parsed.hash32} has no original in the store` };
|
|
13745
13751
|
}
|
|
@@ -14115,16 +14121,18 @@ function createKeyBuilders(prefixes = ASSET_PREFIXES) {
|
|
|
14115
14121
|
stagingKey: (uuid) => `${prefixes.staging}/${uuid}`,
|
|
14116
14122
|
metaKey: (hash32) => `${prefixes.meta}/${hash32}.json`,
|
|
14117
14123
|
metaPrefix: () => `${prefixes.meta}/`,
|
|
14124
|
+
hiddenKey: (hash32) => `${prefixes.hidden}/${hash32}.json`,
|
|
14125
|
+
hiddenPrefix: () => `${prefixes.hidden}/`,
|
|
14118
14126
|
publicKey: (hash32, slug, ext) => `${prefixes.public}/${hash32}/${slug}.${ext}`
|
|
14119
14127
|
};
|
|
14120
14128
|
}
|
|
14121
|
-
var keyBuilders, originalKey, stagingKey, metaKey, publicKey, originalPrefix, metaPrefix;
|
|
14129
|
+
var keyBuilders, originalKey, stagingKey, metaKey, hiddenKey, publicKey, originalPrefix, metaPrefix;
|
|
14122
14130
|
var init_keys = __esm({
|
|
14123
14131
|
"dist/assets/keys.js"() {
|
|
14124
14132
|
"use strict";
|
|
14125
14133
|
init_asset_prefixes();
|
|
14126
14134
|
keyBuilders = createKeyBuilders();
|
|
14127
|
-
({ originalKey, stagingKey, metaKey, publicKey } = keyBuilders);
|
|
14135
|
+
({ originalKey, stagingKey, metaKey, hiddenKey, publicKey } = keyBuilders);
|
|
14128
14136
|
({ originalPrefix, metaPrefix } = keyBuilders);
|
|
14129
14137
|
}
|
|
14130
14138
|
});
|
|
@@ -14140,15 +14148,18 @@ function matchesAwsError(err2, name, httpStatusCode) {
|
|
|
14140
14148
|
const shaped = err2;
|
|
14141
14149
|
return shaped.name === name || shaped.$metadata?.httpStatusCode === httpStatusCode;
|
|
14142
14150
|
}
|
|
14143
|
-
var DEFAULT_MAX_UPLOAD_BYTES, PRESIGN_EXPIRY_SECONDS, PRESIGNED_READ_EXPIRY_SECONDS, isPreconditionFailed, isConditionalRequestConflict, CONFLICT_ATTEMPTS, CONFLICT_BASE_DELAY_MS, isNoSuchKey, S3AssetStore;
|
|
14151
|
+
var DEFAULT_MAX_UPLOAD_BYTES, JSON_SUFFIX, PRESIGN_EXPIRY_SECONDS, PRESIGNED_READ_WINDOW_SECONDS, PRESIGNED_READ_EXPIRY_SECONDS, CREDENTIAL_EXPIRY_MARGIN_SECONDS, isPreconditionFailed, isConditionalRequestConflict, CONFLICT_ATTEMPTS, CONFLICT_BASE_DELAY_MS, isNoSuchKey, S3AssetStore;
|
|
14144
14152
|
var init_store_s3 = __esm({
|
|
14145
14153
|
"dist/assets/store-s3.js"() {
|
|
14146
14154
|
"use strict";
|
|
14147
14155
|
init_sanitize_href3();
|
|
14148
14156
|
init_keys();
|
|
14149
14157
|
DEFAULT_MAX_UPLOAD_BYTES = 50 * 1024 * 1024;
|
|
14158
|
+
JSON_SUFFIX = ".json";
|
|
14150
14159
|
PRESIGN_EXPIRY_SECONDS = 15 * 60;
|
|
14151
|
-
|
|
14160
|
+
PRESIGNED_READ_WINDOW_SECONDS = 60 * 60;
|
|
14161
|
+
PRESIGNED_READ_EXPIRY_SECONDS = 2 * PRESIGNED_READ_WINDOW_SECONDS;
|
|
14162
|
+
CREDENTIAL_EXPIRY_MARGIN_SECONDS = 60;
|
|
14152
14163
|
isPreconditionFailed = (err2) => matchesAwsError(err2, "PreconditionFailed", 412);
|
|
14153
14164
|
isConditionalRequestConflict = (err2) => err2 instanceof Error && err2.name === "ConditionalRequestConflict";
|
|
14154
14165
|
CONFLICT_ATTEMPTS = 4;
|
|
@@ -14341,7 +14352,10 @@ var init_store_s3 = __esm({
|
|
|
14341
14352
|
throw err2;
|
|
14342
14353
|
}
|
|
14343
14354
|
}
|
|
14344
|
-
|
|
14355
|
+
listPublicObjectKeys(prefix) {
|
|
14356
|
+
return this.listKeys(prefix);
|
|
14357
|
+
}
|
|
14358
|
+
async *listKeys(prefix) {
|
|
14345
14359
|
const pages = paginateListObjectsV2({ client: this.client }, { Bucket: this.bucket, Prefix: prefix });
|
|
14346
14360
|
for await (const page of pages) {
|
|
14347
14361
|
for (const object of page.Contents ?? []) {
|
|
@@ -14350,13 +14364,24 @@ var init_store_s3 = __esm({
|
|
|
14350
14364
|
}
|
|
14351
14365
|
}
|
|
14352
14366
|
}
|
|
14353
|
-
/**
|
|
14367
|
+
/**
|
|
14368
|
+
* HEADs first: signing is local and succeeds for a missing key too. A URL signed with temporary
|
|
14369
|
+
* credentials dies when they expire, whatever its own expiry, so `maxAgeSeconds` also stops short
|
|
14370
|
+
* of the credentials' `expiration` when the provider reports one.
|
|
14371
|
+
*/
|
|
14354
14372
|
async presignPublicObjectRead(key) {
|
|
14355
14373
|
if (!await this.hasPublicObject(key))
|
|
14356
14374
|
return null;
|
|
14357
|
-
|
|
14358
|
-
|
|
14359
|
-
|
|
14375
|
+
const { expiration } = await this.client.config.credentials();
|
|
14376
|
+
const nowSeconds = Math.floor(Date.now() / 1e3);
|
|
14377
|
+
const windowStart = nowSeconds - nowSeconds % PRESIGNED_READ_WINDOW_SECONDS;
|
|
14378
|
+
const url = await getSignedUrl(this.client, new GetObjectCommand({ Bucket: this.bucket, Key: key }), { expiresIn: PRESIGNED_READ_EXPIRY_SECONDS, signingDate: new Date(windowStart * 1e3) });
|
|
14379
|
+
let maxAgeSeconds = windowStart + PRESIGNED_READ_WINDOW_SECONDS - nowSeconds;
|
|
14380
|
+
if (expiration) {
|
|
14381
|
+
const credentialSeconds = Math.floor(expiration.getTime() / 1e3) - nowSeconds;
|
|
14382
|
+
maxAgeSeconds = Math.min(maxAgeSeconds, credentialSeconds - CREDENTIAL_EXPIRY_MARGIN_SECONDS);
|
|
14383
|
+
}
|
|
14384
|
+
return { url, maxAgeSeconds: Math.max(0, maxAgeSeconds) };
|
|
14360
14385
|
}
|
|
14361
14386
|
async putMetaIfAbsent(hash32, meta) {
|
|
14362
14387
|
return this.putIfAbsent({
|
|
@@ -14371,27 +14396,67 @@ var init_store_s3 = __esm({
|
|
|
14371
14396
|
return null;
|
|
14372
14397
|
return JSON.parse(Buffer.from(bytes).toString("utf-8"));
|
|
14373
14398
|
}
|
|
14399
|
+
/**
|
|
14400
|
+
* One page of meta keys, filtered by a full listing of the hidden markers: few files are hidden,
|
|
14401
|
+
* and a listing needs only `s3:ListBucket`. A hidden file's meta is fetched only for
|
|
14402
|
+
* `includeHidden`.
|
|
14403
|
+
*/
|
|
14374
14404
|
async listMeta(input) {
|
|
14375
|
-
const
|
|
14376
|
-
|
|
14377
|
-
|
|
14378
|
-
|
|
14379
|
-
|
|
14380
|
-
|
|
14405
|
+
const metaPrefix2 = this.keys.metaPrefix();
|
|
14406
|
+
const [listed, hidden] = await Promise.all([
|
|
14407
|
+
this.client.send(new ListObjectsV2Command({
|
|
14408
|
+
Bucket: this.bucket,
|
|
14409
|
+
Prefix: metaPrefix2,
|
|
14410
|
+
MaxKeys: input?.limit,
|
|
14411
|
+
ContinuationToken: input?.cursor
|
|
14412
|
+
})),
|
|
14413
|
+
this.listHiddenHashes()
|
|
14414
|
+
]);
|
|
14381
14415
|
const objectKeys = (listed.Contents ?? []).map((obj) => obj.Key).filter((key) => Boolean(key));
|
|
14382
14416
|
const fetched = await Promise.all(objectKeys.map(async (key) => {
|
|
14417
|
+
const isHidden = hidden.has(key.slice(metaPrefix2.length, -JSON_SUFFIX.length));
|
|
14418
|
+
if (isHidden && !input?.includeHidden)
|
|
14419
|
+
return null;
|
|
14383
14420
|
const bytes = await this.getObjectBytes(key);
|
|
14384
14421
|
if (!bytes)
|
|
14385
14422
|
return null;
|
|
14386
|
-
|
|
14423
|
+
const meta = JSON.parse(Buffer.from(bytes).toString("utf-8"));
|
|
14424
|
+
return isHidden ? { ...meta, hidden: true } : meta;
|
|
14387
14425
|
}));
|
|
14388
14426
|
return {
|
|
14389
14427
|
items: fetched.filter((meta) => meta !== null),
|
|
14390
14428
|
nextCursor: listed.IsTruncated ? listed.NextContinuationToken : void 0
|
|
14391
14429
|
};
|
|
14392
14430
|
}
|
|
14393
|
-
async
|
|
14394
|
-
|
|
14431
|
+
async listHiddenHashes() {
|
|
14432
|
+
const prefix = this.keys.hiddenPrefix();
|
|
14433
|
+
const hashes = /* @__PURE__ */ new Set();
|
|
14434
|
+
for await (const key of this.listKeys(prefix)) {
|
|
14435
|
+
if (key.endsWith(JSON_SUFFIX))
|
|
14436
|
+
hashes.add(key.slice(prefix.length, -JSON_SUFFIX.length));
|
|
14437
|
+
}
|
|
14438
|
+
return hashes;
|
|
14439
|
+
}
|
|
14440
|
+
async hideAsset(hash32, marker) {
|
|
14441
|
+
return this.putIfAbsent({
|
|
14442
|
+
Key: this.keys.hiddenKey(hash32),
|
|
14443
|
+
Body: JSON.stringify(marker),
|
|
14444
|
+
ContentType: "application/json"
|
|
14445
|
+
});
|
|
14446
|
+
}
|
|
14447
|
+
/**
|
|
14448
|
+
* Lists before deleting: a DELETE of an absent key writes a delete marker on a versioned bucket.
|
|
14449
|
+
* The listing needs only `s3:ListBucket` covering `asset-hidden/` (a bucket-wide grant does; a
|
|
14450
|
+
* policy scoping it by prefix must name this one), no object permission, so finalize can call
|
|
14451
|
+
* this on every re-upload under a role that has never hidden a file.
|
|
14452
|
+
*/
|
|
14453
|
+
async unhideAsset(hash32) {
|
|
14454
|
+
const key = this.keys.hiddenKey(hash32);
|
|
14455
|
+
const listed = await this.client.send(new ListObjectsV2Command({ Bucket: this.bucket, Prefix: key, MaxKeys: 1 }));
|
|
14456
|
+
if (listed.Contents?.[0]?.Key !== key)
|
|
14457
|
+
return false;
|
|
14458
|
+
await this.client.send(new DeleteObjectCommand({ Bucket: this.bucket, Key: key }));
|
|
14459
|
+
return true;
|
|
14395
14460
|
}
|
|
14396
14461
|
async getObject(key) {
|
|
14397
14462
|
try {
|
|
@@ -14766,7 +14831,14 @@ var init_asset_refs = __esm({
|
|
|
14766
14831
|
import fs26 from "node:fs/promises";
|
|
14767
14832
|
import path29 from "node:path";
|
|
14768
14833
|
import { randomUUID as randomUUID4 } from "node:crypto";
|
|
14769
|
-
|
|
14834
|
+
function compareNewestFirst(a, b) {
|
|
14835
|
+
if (a.uploadedAt !== b.uploadedAt)
|
|
14836
|
+
return a.uploadedAt < b.uploadedAt ? 1 : -1;
|
|
14837
|
+
if (a.hash32 === b.hash32)
|
|
14838
|
+
return 0;
|
|
14839
|
+
return a.hash32 < b.hash32 ? 1 : -1;
|
|
14840
|
+
}
|
|
14841
|
+
var DEFAULT_MAX_UPLOAD_BYTES2, DEFAULT_LIST_LIMIT, HEADERS_SIDECAR_SUFFIX, TEMP_DIR, ORIGINAL_EXT_RE, HIDDEN_MARKER_RE, LocalAssetStore;
|
|
14770
14842
|
var init_store_local = __esm({
|
|
14771
14843
|
"dist/assets/store-local.js"() {
|
|
14772
14844
|
"use strict";
|
|
@@ -14778,6 +14850,7 @@ var init_store_local = __esm({
|
|
|
14778
14850
|
HEADERS_SIDECAR_SUFFIX = ".headers.json";
|
|
14779
14851
|
TEMP_DIR = ".asset-tmp";
|
|
14780
14852
|
ORIGINAL_EXT_RE = /^[a-z0-9]+$/;
|
|
14853
|
+
HIDDEN_MARKER_RE = /^([a-f0-9]{32})\.json$/;
|
|
14781
14854
|
LocalAssetStore = class {
|
|
14782
14855
|
constructor(options) {
|
|
14783
14856
|
this.capabilities = { directUpload: false };
|
|
@@ -14849,11 +14922,12 @@ var init_store_local = __esm({
|
|
|
14849
14922
|
}
|
|
14850
14923
|
}
|
|
14851
14924
|
/**
|
|
14852
|
-
* Create-only write of a blob and its headers sidecar. Each is written
|
|
14853
|
-
* `link()`ed into place, which fails with EEXIST rather than replacing
|
|
14854
|
-
* The sidecar is linked first, so a reader never finds a blob without
|
|
14855
|
-
* already there was linked by an earlier writer of this key and is kept,
|
|
14856
|
-
* first writer's headers win, as on S3 (a static's Content-Disposition names
|
|
14925
|
+
* Create-only write of a blob and, when `headers` is given, its headers sidecar. Each is written
|
|
14926
|
+
* whole to a temp file, then `link()`ed into place, which fails with EEXIST rather than replacing
|
|
14927
|
+
* (see docs/concurrency.md). The sidecar is linked first, so a reader never finds a blob without
|
|
14928
|
+
* its headers. A sidecar already there was linked by an earlier writer of this key and is kept,
|
|
14929
|
+
* as the blob is: the first writer's headers win, as on S3 (a static's Content-Disposition names
|
|
14930
|
+
* its uploaded file).
|
|
14857
14931
|
*/
|
|
14858
14932
|
async createExclusive(key, data, headers) {
|
|
14859
14933
|
const filePath = this.resolveKey(key);
|
|
@@ -14866,8 +14940,10 @@ var init_store_local = __esm({
|
|
|
14866
14940
|
const tempHeaders = path29.join(tempDir, randomUUID4());
|
|
14867
14941
|
try {
|
|
14868
14942
|
await fs26.writeFile(tempBlob, data);
|
|
14869
|
-
|
|
14870
|
-
|
|
14943
|
+
if (headers) {
|
|
14944
|
+
await fs26.writeFile(tempHeaders, JSON.stringify(headers), "utf-8");
|
|
14945
|
+
await this.linkIfAbsent(tempHeaders, this.headersSidecarPath(filePath));
|
|
14946
|
+
}
|
|
14871
14947
|
return await this.linkIfAbsent(tempBlob, filePath) ? "created" : "already-exists";
|
|
14872
14948
|
} finally {
|
|
14873
14949
|
await Promise.all([tempBlob, tempHeaders].map((temp) => fs26.unlink(temp).catch(() => {
|
|
@@ -14999,6 +15075,11 @@ var init_store_local = __esm({
|
|
|
14999
15075
|
throw err2;
|
|
15000
15076
|
}
|
|
15001
15077
|
}
|
|
15078
|
+
/**
|
|
15079
|
+
* The cursor names the last item returned, and the next page starts after it in sort order, so a
|
|
15080
|
+
* page boundary holds when that item is hidden in between: its meta is never deleted, and the
|
|
15081
|
+
* comparison needs only its place, not its presence in the filtered list.
|
|
15082
|
+
*/
|
|
15002
15083
|
async listMeta(input) {
|
|
15003
15084
|
const limit = input?.limit ?? DEFAULT_LIST_LIMIT;
|
|
15004
15085
|
const dir = this.resolveKey(ASSET_PREFIXES.meta);
|
|
@@ -15022,29 +15103,43 @@ var init_store_local = __esm({
|
|
|
15022
15103
|
}
|
|
15023
15104
|
}
|
|
15024
15105
|
}
|
|
15025
|
-
all.sort(
|
|
15026
|
-
|
|
15027
|
-
|
|
15028
|
-
return a.hash32 < b.hash32 ? 1 : -1;
|
|
15029
|
-
});
|
|
15106
|
+
all.sort(compareNewestFirst);
|
|
15107
|
+
const hidden = await this.hiddenHashes();
|
|
15108
|
+
const listed = input?.includeHidden ? all.map((meta) => hidden.has(meta.hash32) ? { ...meta, hidden: true } : meta) : all.filter((meta) => !hidden.has(meta.hash32));
|
|
15030
15109
|
let startIndex = 0;
|
|
15031
15110
|
if (input?.cursor) {
|
|
15032
15111
|
const afterHash32 = Buffer.from(input.cursor, "base64").toString("utf-8");
|
|
15033
|
-
const
|
|
15034
|
-
|
|
15112
|
+
const anchor = all.find((m) => m.hash32 === afterHash32);
|
|
15113
|
+
if (anchor) {
|
|
15114
|
+
const after = listed.findIndex((m) => compareNewestFirst(m, anchor) > 0);
|
|
15115
|
+
startIndex = after === -1 ? listed.length : after;
|
|
15116
|
+
}
|
|
15035
15117
|
}
|
|
15036
|
-
const items =
|
|
15037
|
-
const hasMore = startIndex + limit <
|
|
15118
|
+
const items = listed.slice(startIndex, startIndex + limit);
|
|
15119
|
+
const hasMore = startIndex + limit < listed.length;
|
|
15038
15120
|
const nextCursor = hasMore ? Buffer.from(items[items.length - 1].hash32, "utf-8").toString("base64") : void 0;
|
|
15039
15121
|
return { items, nextCursor };
|
|
15040
15122
|
}
|
|
15041
|
-
async
|
|
15042
|
-
const filePath = this.resolveKey(metaKey(hash32));
|
|
15123
|
+
async hiddenHashes() {
|
|
15043
15124
|
try {
|
|
15044
|
-
await fs26.
|
|
15125
|
+
const names = await fs26.readdir(this.resolveKey(ASSET_PREFIXES.hidden));
|
|
15126
|
+
return new Set(names.flatMap((name) => HIDDEN_MARKER_RE.exec(name)?.[1] ?? []));
|
|
15045
15127
|
} catch (err2) {
|
|
15046
15128
|
if (isNotFoundError(err2))
|
|
15047
|
-
return;
|
|
15129
|
+
return /* @__PURE__ */ new Set();
|
|
15130
|
+
throw err2;
|
|
15131
|
+
}
|
|
15132
|
+
}
|
|
15133
|
+
async hideAsset(hash32, marker) {
|
|
15134
|
+
return this.createExclusive(hiddenKey(hash32), new TextEncoder().encode(JSON.stringify(marker)));
|
|
15135
|
+
}
|
|
15136
|
+
async unhideAsset(hash32) {
|
|
15137
|
+
try {
|
|
15138
|
+
await fs26.unlink(this.resolveKey(hiddenKey(hash32)));
|
|
15139
|
+
return true;
|
|
15140
|
+
} catch (err2) {
|
|
15141
|
+
if (isNotFoundError(err2))
|
|
15142
|
+
return false;
|
|
15048
15143
|
throw err2;
|
|
15049
15144
|
}
|
|
15050
15145
|
}
|
package/dist/config/types.d.ts
CHANGED
|
@@ -144,8 +144,9 @@ export interface ReferenceFieldConfig extends BaseFieldConfig {
|
|
|
144
144
|
export interface ImageFieldConfig extends BaseFieldConfig {
|
|
145
145
|
type: 'image';
|
|
146
146
|
/**
|
|
147
|
-
* "W:H" aspect ratio (e.g. "16:9", "1:1")
|
|
148
|
-
*
|
|
147
|
+
* "W:H" aspect ratio (e.g. "16:9", "1:1"). Picking an image opens a crop step locked to it,
|
|
148
|
+
* and Crop keeps that shape. Without it, a pick isn't cropped and Crop is free-form.
|
|
149
|
+
* Validated at config-parse time: positive integers on both sides.
|
|
149
150
|
*/
|
|
150
151
|
aspect?: string;
|
|
151
152
|
/** Allow an empty `alt`. Default: alt text is required (accessibility). */
|
|
@@ -208,5 +208,6 @@ export const BranchManager = ({ branches, mode, user, currentBranchName, current
|
|
|
208
208
|
...branches.filter((b) => b.name !== currentBranchName),
|
|
209
209
|
]
|
|
210
210
|
: branches;
|
|
211
|
-
|
|
211
|
+
const hasBranchThreads = comments.some((t) => t.type === 'branch');
|
|
212
|
+
return (_jsxs(Stack, { h: "100%", style: { display: 'flex', flexDirection: 'column' }, gap: 0, "data-testid": "branch-manager", children: [currentUserId && onAddComment && onResolveThread && hasBranchThreads && (_jsx(Stack, { pt: "sm", children: _jsx(BranchComments, { comments: comments, currentUserId: currentUserId, canResolve: canResolve, onAddComment: onAddComment, onResolveThread: onResolveThread, highlightThreadId: highlightThreadId, onGetUserMetadata: onGetCommentUserMetadata }) })), !!supportsBranching && (_jsxs(Stack, { gap: "sm", pt: "sm", children: [_jsx(Button, { variant: "light", size: "sm", fullWidth: true, onClick: () => setShowCreateForm(!showCreateForm), disabled: creating, "data-testid": "create-branch-button", children: showCreateForm ? 'Cancel' : `${EDITOR_ACTIONS.newBranch}…` }), _jsx(Collapse, { in: showCreateForm, children: _jsx(Paper, { withBorder: true, p: "md", children: _jsxs(Stack, { gap: "sm", children: [_jsx(TextInput, { label: "Branch name", placeholder: "feature/my-branch", value: newBranchName, onChange: (e) => setNewBranchName(e.target.value), required: true, disabled: creating, "data-testid": "branch-name-input" }), _jsx(TextInput, { label: "Title", placeholder: "Brief description", value: newBranchTitle, onChange: (e) => setNewBranchTitle(e.target.value), disabled: creating, "data-testid": "branch-title-input" }), _jsx(Textarea, { label: "Description", placeholder: "Detailed description of the changes", value: newBranchDescription, onChange: (e) => setNewBranchDescription(e.target.value), minRows: 2, disabled: creating, "data-testid": "branch-description-textarea" }), _jsx(Button, { size: "sm", onClick: handleCreate, disabled: !newBranchName.trim(), loading: creating, fullWidth: true, "data-testid": "create-branch-submit", children: "Create branch" })] }) }) })] })), _jsx(ScrollArea, { style: { flex: 1 }, pb: "md", children: branches.length === 0 ? (_jsx(Text, { size: "sm", c: "dimmed", py: "md", children: !supportsBranching ? 'Branch management is not available.' : 'No branches available.' })) : (_jsx(Stack, { gap: "sm", children: orderedBranches.map((b) => (_jsx(BranchCard, { branch: b, isCurrent: b.name === currentBranchName, workflowBlockedReason: b.name === currentBranchName ? currentWorkflowBlockedReason : undefined, user: user, onSelect: onSelect, onSubmit: onSubmit, onWithdraw: onWithdraw, onRequestChanges: onRequestChanges, onDelete: onDelete, onGetUserMetadata: onGetUserMetadata, onGetOwnerMetadata: onGetOwnerMetadata }, b.name))) })) })] }));
|
|
212
213
|
};
|
|
@@ -1,23 +1,17 @@
|
|
|
1
1
|
'use client';
|
|
2
|
-
import {
|
|
2
|
+
import { jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
|
|
3
3
|
import { useId, useRef, useState } from 'react';
|
|
4
|
-
import { Badge, Button,
|
|
4
|
+
import { Badge, Button, Group, Paper, ScrollArea, Stack, Text, Textarea, Switch, Tooltip, VisuallyHidden, } from '@mantine/core';
|
|
5
|
+
import { IconChevronRight } from '@tabler/icons-react';
|
|
5
6
|
import { UserBadge } from './components/UserBadge.js';
|
|
6
7
|
import { userIdLabel } from './user-display.js';
|
|
8
|
+
import { OverlayDrawer } from './shell/OverlayHeader.js';
|
|
7
9
|
import { RelativeTime } from './components/RelativeTime.js';
|
|
8
10
|
import { ThreadReplyControls } from './comments/ThreadReplyControls.js';
|
|
9
11
|
import { useTrackedWrites } from './comments/use-tracked-writes.js';
|
|
10
12
|
import { CloseRefusedNotice } from './comments/CloseRefusedNotice.js';
|
|
11
13
|
import { ON_REMOVED_ITEM } from './comments/item-anchor-copy.js';
|
|
12
14
|
import { CommentWriteAlert, toCommentWriteFailure, } from './comments/CommentWriteAlert.js';
|
|
13
|
-
/**
|
|
14
|
-
* A jump button that stays focusable while a write is in flight (`aria-disabled`, not `disabled`),
|
|
15
|
-
* so its reason opens on keyboard focus and touch as well as hover.
|
|
16
|
-
*/
|
|
17
|
-
const JumpButton = ({ blocked, onClick, children }) => (_jsx(Tooltip, { label: "Your change is still saving. Try again once it finishes.", disabled: !blocked, multiline: true, maw: 280, children: _jsx(Button, { variant: "light", onClick: () => {
|
|
18
|
-
if (!blocked)
|
|
19
|
-
onClick();
|
|
20
|
-
}, "data-disabled": blocked ? true : undefined, "aria-disabled": blocked ? true : undefined, children: children }) }));
|
|
21
15
|
const THREAD_KIND_LABELS = { field: 'Field', entry: 'Entry', branch: 'Branch' };
|
|
22
16
|
export const CommentsPanel = ({ opened, branchName, comments, canResolve, currentUserId, onAddComment, onResolveThread, onClose, onJumpToField, onJumpToEntry, onJumpToBranch, onGetUserMetadata, entryLabel, fieldLabel, }) => {
|
|
23
17
|
const [newCommentText, setNewCommentText] = useState('');
|
|
@@ -65,27 +59,71 @@ export const CommentsPanel = ({ opened, branchName, comments, canResolve, curren
|
|
|
65
59
|
setIsSubmitting(false);
|
|
66
60
|
}
|
|
67
61
|
};
|
|
68
|
-
|
|
69
|
-
|
|
62
|
+
// "Entry › Field", or undefined for a thread on the branch itself.
|
|
63
|
+
const threadPath = (thread) => {
|
|
70
64
|
if (!thread.entryPath)
|
|
71
|
-
return
|
|
65
|
+
return undefined;
|
|
72
66
|
const entry = entryLabel?.(thread.entryPath) ?? thread.entryPath;
|
|
73
67
|
const field = thread.canopyPath ? (fieldLabel?.(thread) ?? thread.canopyPath) : undefined;
|
|
74
|
-
return
|
|
68
|
+
return field ? `${entry} › ${field}` : entry;
|
|
69
|
+
};
|
|
70
|
+
const threadHeading = (thread) => {
|
|
71
|
+
const kind = THREAD_KIND_LABELS[thread.type];
|
|
72
|
+
const path = threadPath(thread);
|
|
73
|
+
return path ? `${kind} · ${path}` : kind;
|
|
74
|
+
};
|
|
75
|
+
const jumpTarget = (thread) => {
|
|
76
|
+
const { entryPath, canopyPath } = thread;
|
|
77
|
+
if (thread.type === 'field' &&
|
|
78
|
+
entryPath &&
|
|
79
|
+
canopyPath &&
|
|
80
|
+
!thread.onRemovedItem &&
|
|
81
|
+
onJumpToField) {
|
|
82
|
+
return { label: 'Jump to field', go: () => onJumpToField(entryPath, canopyPath, thread.id) };
|
|
83
|
+
}
|
|
84
|
+
// A removed item's thread lives on its entry.
|
|
85
|
+
if ((thread.type === 'entry' || thread.onRemovedItem) && entryPath && onJumpToEntry) {
|
|
86
|
+
return { label: 'Jump to entry', go: () => onJumpToEntry(entryPath, thread.id) };
|
|
87
|
+
}
|
|
88
|
+
if (thread.type === 'branch' && onJumpToBranch) {
|
|
89
|
+
return { label: 'Open branch discussion', go: () => onJumpToBranch(thread.id) };
|
|
90
|
+
}
|
|
91
|
+
return null;
|
|
92
|
+
};
|
|
93
|
+
const setHeadingRef = (threadId, node) => {
|
|
94
|
+
if (node)
|
|
95
|
+
headingRefs.current.set(threadId, node);
|
|
96
|
+
else
|
|
97
|
+
headingRefs.current.delete(threadId);
|
|
75
98
|
};
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
99
|
+
// The heading's text names the article; the removed-item badge beside it is outside it.
|
|
100
|
+
const headingText = (thread) => (_jsxs("span", { id: headingId(thread.id), "data-testid": "comment-thread-heading", children: [threadHeading(thread), thread.onRemovedItem && _jsxs(VisuallyHidden, { children: [", ", ON_REMOVED_ITEM.toLowerCase()] })] }));
|
|
101
|
+
return (_jsx(OverlayDrawer, { opened: opened, onClose: close, returnFocus: !jumping, position: "right", size: 420, title: "Comments", children: _jsxs(Stack, { gap: "md", h: "100%", style: { display: 'flex', flexDirection: 'column' }, children: [_jsx(Text, { size: "xs", c: "dimmed", children: "Select a comment's heading to see it where it was left." }), _jsx(CloseRefusedNotice, { refused: closeRefused }), _jsx(Paper, { withBorder: true, p: "md", children: _jsxs(Stack, { gap: "sm", children: [_jsxs(Text, { size: "xs", c: "dimmed", children: ["Posts a branch-level comment on \"", branchName, "\". To comment on a field, use its comment icon."] }), _jsx(Textarea, { placeholder: "Add a comment\u2026", "aria-label": "New branch comment", value: newCommentText, onChange: (e) => setNewCommentText(e.target.value), minRows: 3, disabled: isSubmitting, "data-testid": "comment-textarea" }), error && _jsx(CommentWriteAlert, { failure: error, onClose: () => setError(null) }), _jsx(Button, { onClick: handleAddComment, size: "sm", loading: isSubmitting, disabled: !newCommentText.trim(), "data-testid": "comment-submit", children: "Comment" })] }) }), _jsxs(Group, { justify: "space-between", children: [_jsxs(Text, { ref: threadCountRef, size: "sm", fw: 500, tabIndex: -1, children: [filteredThreads.length, " ", filteredThreads.length === 1 ? 'thread' : 'threads'] }), _jsx(Switch, { label: "Show resolved", checked: showResolved, onChange: (e) => setShowResolved(e.currentTarget.checked), size: "sm" })] }), _jsx(ScrollArea, { style: { flex: 1 }, children: filteredThreads.length === 0 ? (_jsx(Text, { size: "sm", c: "dimmed", ta: "center", py: "xl", children: showResolved ? 'No comments yet' : 'No unresolved comments' })) : (_jsx(Stack, { gap: "md", children: filteredThreads.map((thread) => {
|
|
102
|
+
const target = jumpTarget(thread);
|
|
103
|
+
return (_jsx(Paper, { withBorder: true, p: "md", component: "article", "aria-labelledby": headingId(thread.id), bg: thread.resolved ? 'var(--mantine-color-default-hover)' : undefined, "data-testid": "comment-thread", children: _jsxs(Stack, { gap: "xs", children: [_jsxs(Group, { gap: "xs", children: [target ? (
|
|
104
|
+
// The heading is the jump control, the one tab stop on the card, in an
|
|
105
|
+
// `h3` under the drawer's `h2` so it is reachable by heading. It stays
|
|
106
|
+
// focusable while a write is in flight (`aria-disabled`, not `disabled`),
|
|
107
|
+
// so its reason opens on keyboard focus and touch as well as hover, and
|
|
108
|
+
// keeps its text colour: only the cursor says it is waiting. The visible
|
|
109
|
+
// text is inside the button's name (WCAG 2.5.3), after a hidden prefix
|
|
110
|
+
// that says what it does.
|
|
111
|
+
_jsx(Text, { component: "h3", size: "xs", fw: 600, m: 0, children: _jsx(Tooltip, { label: "Your change is still saving. Try again once it finishes.", disabled: inFlight === 0, multiline: true, maw: 280, children: _jsxs(Button, { ref: (node) => setHeadingRef(thread.id, node), variant: "subtle", color: "neutral", h: "auto", px: 6, py: 2, mx: -6, rightSection: _jsx(IconChevronRight, { size: 14, "aria-hidden": true }), "aria-disabled": inFlight > 0 ? true : undefined, styles: {
|
|
112
|
+
root: { cursor: inFlight > 0 ? 'default' : 'pointer' },
|
|
113
|
+
label: {
|
|
114
|
+
whiteSpace: 'normal',
|
|
115
|
+
textAlign: 'start',
|
|
116
|
+
lineHeight: 1.35,
|
|
117
|
+
overflowWrap: 'anywhere',
|
|
118
|
+
},
|
|
119
|
+
}, onClick: () => {
|
|
120
|
+
if (inFlight === 0)
|
|
121
|
+
jump(target.go);
|
|
122
|
+
}, children: [_jsx(VisuallyHidden, { component: "span", children: `${target.label}: ` }), headingText(thread)] }) }) })) : (_jsx(Text, { component: "h3", ref: (node) => setHeadingRef(thread.id, node), size: "xs", fw: 600, m: 0, tabIndex: -1, children: headingText(thread) })), thread.resolved && (_jsx(Badge, { size: "xs", color: "green", variant: "light", children: "Resolved" })), thread.onRemovedItem && (_jsx(Badge, { size: "xs", color: "gray", variant: "light", c: "var(--mantine-color-text)", children: ON_REMOVED_ITEM }))] }), thread.comments.map((comment, idx) => (_jsxs("div", { children: [idx > 0 && (_jsx("div", { style: {
|
|
123
|
+
borderTop: '1px solid var(--mantine-color-default-border)',
|
|
124
|
+
margin: '8px 0',
|
|
125
|
+
} })), _jsxs(Stack, { gap: 4, children: [_jsxs(Group, { gap: "xs", children: [onGetUserMetadata ? (_jsx(UserBadge, { userId: comment.userId, getUserMetadata: onGetUserMetadata, variant: "avatar-name", size: "xs", showEmailTooltip: true, showBadge: true, badgeVariant: "light", color: "gray" })) : (_jsx(Text, { size: "xs", fw: 500, children: userIdLabel(comment.userId) })), _jsx(RelativeTime, { iso: comment.timestamp, size: "xs", c: "dimmed" })] }), _jsx(Text, { size: "sm", style: { whiteSpace: 'pre-wrap' }, children: comment.text })] })] }, comment.id))), !thread.resolved && (_jsx(ThreadReplyControls, { thread: thread, onAddReply: (text) => addComment(text, thread.type, thread.entryPath, thread.canopyPath, thread.id), onResolve: () => resolveThread(thread.id), focusAfterResolve: () => showResolved
|
|
126
|
+
? headingRefs.current.get(thread.id)
|
|
127
|
+
: threadCountRef.current, currentUserId: currentUserId, canResolve: canResolve, replyText: replyDrafts[thread.id] ?? '', onReplyTextChange: (text) => setReplyDrafts((prev) => ({ ...prev, [thread.id]: text })) }))] }) }, thread.id));
|
|
128
|
+
}) })) })] }) }));
|
|
91
129
|
};
|
package/dist/editor/Editor.d.ts
CHANGED
|
@@ -7,7 +7,10 @@ import type { LogicalPath, ContentId } from '../paths/types.js';
|
|
|
7
7
|
export interface EditorEntry {
|
|
8
8
|
path: LogicalPath;
|
|
9
9
|
contentId: ContentId;
|
|
10
|
+
/** What the editor calls the entry: its title, or a labelled singleton's type label. */
|
|
10
11
|
label: string;
|
|
12
|
+
/** The entry's title, kept where `label` is a singleton's type label: link text and search use it. */
|
|
13
|
+
title?: string;
|
|
11
14
|
status?: string;
|
|
12
15
|
schema: EntrySchema;
|
|
13
16
|
previewSrc?: string;
|