@drawnagency/primitives 0.1.77 → 0.1.78
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/dist/{chunk-B2DLHZKC.js → chunk-2DDWWTRX.js} +1 -1
- package/dist/{chunk-S55OZ7F2.js → chunk-3LNKOLZP.js} +4 -0
- package/dist/{chunk-SWIHUIFT.js → chunk-4SIWBWKZ.js} +12 -2
- package/dist/{chunk-RO4E5UJI.js → chunk-BAQYZSFF.js} +6 -6
- package/dist/{chunk-IFHRHV7S.js → chunk-EMGLZQCM.js} +1 -1
- package/dist/{chunk-YP7IHMC3.js → chunk-K4JD6FD3.js} +1 -1
- package/dist/{chunk-SFKYUKVN.js → chunk-MK4POMLZ.js} +1 -1
- package/dist/{chunk-63BQQNZO.js → chunk-NS3UTJ3E.js} +3 -3
- package/dist/{chunk-UUCOJL7O.js → chunk-UA6OCJJK.js} +2 -2
- package/dist/{chunk-XFDEUK37.js → chunk-Z5IYWUW3.js} +1 -1
- package/dist/components/editor/index.js +4 -4
- package/dist/components/primitives/index.js +5 -5
- package/dist/components/sections/brand-guide.js +5 -5
- package/dist/components/sections/register-schemas.js +8 -8
- package/dist/components/sections/register.js +8 -8
- package/dist/components/shell/EditorShell.d.ts.map +1 -1
- package/dist/components/shell/MediaBreadcrumb.d.ts +10 -0
- package/dist/components/shell/MediaBreadcrumb.d.ts.map +1 -0
- package/dist/components/shell/MediaFolderTile.d.ts +15 -0
- package/dist/components/shell/MediaFolderTile.d.ts.map +1 -0
- package/dist/components/shell/MediaFolderTree.d.ts +20 -0
- package/dist/components/shell/MediaFolderTree.d.ts.map +1 -0
- package/dist/components/shell/MediaLibraryModal.d.ts +15 -3
- package/dist/components/shell/MediaLibraryModal.d.ts.map +1 -1
- package/dist/components/shell/MoveToFolderDialog.d.ts +16 -0
- package/dist/components/shell/MoveToFolderDialog.d.ts.map +1 -0
- package/dist/hooks/useEditorPublish.d.ts.map +1 -1
- package/dist/hooks/useMediaFolderDnd.d.ts +27 -0
- package/dist/hooks/useMediaFolderDnd.d.ts.map +1 -0
- package/dist/hooks/useMediaPipeline.d.ts +14 -2
- package/dist/hooks/useMediaPipeline.d.ts.map +1 -1
- package/dist/index.js +8 -4
- package/dist/lib/index.js +2 -2
- package/dist/lib/media-dnd.d.ts +26 -0
- package/dist/lib/media-dnd.d.ts.map +1 -0
- package/dist/lib/media-folders.d.ts +59 -0
- package/dist/lib/media-folders.d.ts.map +1 -0
- package/dist/media/folder-drop.d.ts +22 -0
- package/dist/media/folder-drop.d.ts.map +1 -0
- package/dist/media/index.js +6 -2
- package/dist/media/types.d.ts +1 -1
- package/dist/media/types.d.ts.map +1 -1
- package/dist/media/utils.d.ts +7 -0
- package/dist/media/utils.d.ts.map +1 -1
- package/dist/schemas/index.js +4 -2
- package/dist/schemas/media.d.ts +11 -0
- package/dist/schemas/media.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/components/shell/EditorShell.tsx +20 -4
- package/src/components/shell/MediaBreadcrumb.tsx +77 -0
- package/src/components/shell/MediaFolderTile.tsx +141 -0
- package/src/components/shell/MediaFolderTree.tsx +127 -0
- package/src/components/shell/MediaLibraryModal.tsx +611 -229
- package/src/components/shell/MoveToFolderDialog.tsx +78 -0
- package/src/hooks/useEditorPublish.ts +14 -11
- package/src/hooks/useMediaFolderDnd.ts +129 -0
- package/src/hooks/useMediaPipeline.ts +162 -6
- package/src/lib/media-dnd.ts +62 -0
- package/src/lib/media-folders.ts +232 -0
- package/src/media/folder-drop.ts +62 -0
- package/src/media/types.ts +2 -0
- package/src/media/utils.ts +10 -0
- package/src/schemas/media.ts +13 -0
package/dist/lib/index.js
CHANGED
|
@@ -7,7 +7,7 @@ import {
|
|
|
7
7
|
loadStaticSiteContent,
|
|
8
8
|
mergeSiteContent,
|
|
9
9
|
safeRedirect
|
|
10
|
-
} from "../chunk-
|
|
10
|
+
} from "../chunk-Z5IYWUW3.js";
|
|
11
11
|
import {
|
|
12
12
|
createEvent,
|
|
13
13
|
darkModeEvent,
|
|
@@ -33,7 +33,7 @@ import {
|
|
|
33
33
|
audiencePasses
|
|
34
34
|
} from "../chunk-4YEOTFVU.js";
|
|
35
35
|
import "../chunk-TZQPOR5A.js";
|
|
36
|
-
import "../chunk-
|
|
36
|
+
import "../chunk-4SIWBWKZ.js";
|
|
37
37
|
import {
|
|
38
38
|
clearRegistry,
|
|
39
39
|
createRegistry,
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type { MediaFolder, MediaItem } from "../media/types";
|
|
2
|
+
export interface MediaDragData {
|
|
3
|
+
dragType: "media-item";
|
|
4
|
+
mediaIds: string[];
|
|
5
|
+
[key: string | symbol]: unknown;
|
|
6
|
+
}
|
|
7
|
+
export interface MediaFolderDragData {
|
|
8
|
+
dragType: "media-folder";
|
|
9
|
+
folderId: string;
|
|
10
|
+
[key: string | symbol]: unknown;
|
|
11
|
+
}
|
|
12
|
+
export interface MediaFolderDropData {
|
|
13
|
+
dropType: "media-folder";
|
|
14
|
+
targetFolderId: string | null;
|
|
15
|
+
[key: string | symbol]: unknown;
|
|
16
|
+
}
|
|
17
|
+
export declare function buildMediaDragData(mediaIds: string[]): MediaDragData;
|
|
18
|
+
export declare function buildFolderDragData(folderId: string): MediaFolderDragData;
|
|
19
|
+
export declare function buildFolderDropData(targetFolderId: string | null): MediaFolderDropData;
|
|
20
|
+
export declare function isMediaDragData(d: Record<string | symbol, unknown>): d is MediaDragData;
|
|
21
|
+
export declare function isFolderDragData(d: Record<string | symbol, unknown>): d is MediaFolderDragData;
|
|
22
|
+
/** True when at least one dragged item would actually change folder. */
|
|
23
|
+
export declare function canDropMediaInto(drag: MediaDragData, targetFolderId: string | null, folders: Record<string, MediaFolder>, itemsById: Record<string, MediaItem>): boolean;
|
|
24
|
+
/** Folder drops delegate to the tree-integrity guard (self/descendant/no-op). */
|
|
25
|
+
export declare function canDropFolderInto(drag: MediaFolderDragData, targetFolderId: string | null, folders: Record<string, MediaFolder>): boolean;
|
|
26
|
+
//# sourceMappingURL=media-dnd.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"media-dnd.d.ts","sourceRoot":"","sources":["../../src/lib/media-dnd.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,SAAS,EAAE,MAAM,gBAAgB,CAAC;AAG7D,MAAM,WAAW,aAAa;IAC5B,QAAQ,EAAE,YAAY,CAAC;IACvB,QAAQ,EAAE,MAAM,EAAE,CAAC;IACnB,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,GAAG,OAAO,CAAC;CACjC;AAED,MAAM,WAAW,mBAAmB;IAClC,QAAQ,EAAE,cAAc,CAAC;IACzB,QAAQ,EAAE,MAAM,CAAC;IACjB,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,GAAG,OAAO,CAAC;CACjC;AAED,MAAM,WAAW,mBAAmB;IAClC,QAAQ,EAAE,cAAc,CAAC;IACzB,cAAc,EAAE,MAAM,GAAG,IAAI,CAAC;IAC9B,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,GAAG,OAAO,CAAC;CACjC;AAED,wBAAgB,kBAAkB,CAAC,QAAQ,EAAE,MAAM,EAAE,GAAG,aAAa,CAEpE;AAED,wBAAgB,mBAAmB,CAAC,QAAQ,EAAE,MAAM,GAAG,mBAAmB,CAEzE;AAED,wBAAgB,mBAAmB,CAAC,cAAc,EAAE,MAAM,GAAG,IAAI,GAAG,mBAAmB,CAEtF;AAED,wBAAgB,eAAe,CAAC,CAAC,EAAE,MAAM,CAAC,MAAM,GAAG,MAAM,EAAE,OAAO,CAAC,GAAG,CAAC,IAAI,aAAa,CAEvF;AAED,wBAAgB,gBAAgB,CAAC,CAAC,EAAE,MAAM,CAAC,MAAM,GAAG,MAAM,EAAE,OAAO,CAAC,GAAG,CAAC,IAAI,mBAAmB,CAE9F;AAED,wEAAwE;AACxE,wBAAgB,gBAAgB,CAC9B,IAAI,EAAE,aAAa,EACnB,cAAc,EAAE,MAAM,GAAG,IAAI,EAC7B,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,WAAW,CAAC,EACpC,SAAS,EAAE,MAAM,CAAC,MAAM,EAAE,SAAS,CAAC,GACnC,OAAO,CAKT;AAED,iFAAiF;AACjF,wBAAgB,iBAAiB,CAC/B,IAAI,EAAE,mBAAmB,EACzB,cAAc,EAAE,MAAM,GAAG,IAAI,EAC7B,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,WAAW,CAAC,GACnC,OAAO,CAET"}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import type { MediaFolder, MediaItem, MediaManifest } from "../media/types";
|
|
2
|
+
export interface FolderTreeNode {
|
|
3
|
+
id: string;
|
|
4
|
+
name: string;
|
|
5
|
+
children: FolderTreeNode[];
|
|
6
|
+
}
|
|
7
|
+
export interface FolderUploadPlan {
|
|
8
|
+
/** New folder entities to merge into manifest.folders, keyed by new id. */
|
|
9
|
+
newFolders: Record<string, MediaFolder>;
|
|
10
|
+
/** Display-folder target per uploaded file (null = root). */
|
|
11
|
+
fileTargets: {
|
|
12
|
+
file: File;
|
|
13
|
+
folderId: string | null;
|
|
14
|
+
}[];
|
|
15
|
+
}
|
|
16
|
+
/** Display-folder id. NOT related to MediaItem.folder (the git storage dir). */
|
|
17
|
+
export declare function createFolderId(): string;
|
|
18
|
+
/** True when this folder's parent chain loops back to itself. */
|
|
19
|
+
export declare function isCyclicFolder(folders: Record<string, MediaFolder>, id: string): boolean;
|
|
20
|
+
/**
|
|
21
|
+
* Effective parent for rendering: null (root) when the parent is missing,
|
|
22
|
+
* dangling, or `id` itself sits on a parent cycle. Cycle members re-root,
|
|
23
|
+
* which breaks the loop for their (kept) descendants — corrupt manifests
|
|
24
|
+
* always render, and healMediaManifest persists this view on save.
|
|
25
|
+
*/
|
|
26
|
+
export declare function effectiveParentId(folders: Record<string, MediaFolder>, id: string): string | null;
|
|
27
|
+
/** Direct children of `parentId`, alphabetically (case-insensitive). */
|
|
28
|
+
export declare function childFolderIds(folders: Record<string, MediaFolder>, parentId: string | null): string[];
|
|
29
|
+
export declare function buildFolderTree(folders: Record<string, MediaFolder>): FolderTreeNode[];
|
|
30
|
+
/** Names from root to `id` inclusive, cycle-safe. */
|
|
31
|
+
export declare function folderPathNames(folders: Record<string, MediaFolder>, id: string): string[];
|
|
32
|
+
/** Folder ids from root to `id` inclusive, cycle-safe. */
|
|
33
|
+
export declare function folderPathIds(folders: Record<string, MediaFolder>, id: string): string[];
|
|
34
|
+
/** All folders under `id` (by effective parent), INCLUDING `id` itself. */
|
|
35
|
+
export declare function descendantFolderIds(folders: Record<string, MediaFolder>, id: string): Set<string>;
|
|
36
|
+
/** May `id` re-parent to `targetParentId`? Blocks self/descendant targets and no-ops. */
|
|
37
|
+
export declare function canMoveFolder(folders: Record<string, MediaFolder>, id: string, targetParentId: string | null): boolean;
|
|
38
|
+
/** Trimmed `desired`, auto-suffixed " (2)", " (3)"… on case-insensitive sibling collision. */
|
|
39
|
+
export declare function uniqueFolderName(folders: Record<string, MediaFolder>, parentId: string | null, desired: string, excludeId?: string): string;
|
|
40
|
+
/** The folder an item renders in: its folderId when it points at a real folder, else root. */
|
|
41
|
+
export declare function itemFolderId(folders: Record<string, MediaFolder>, item: Pick<MediaItem, "folderId">): string | null;
|
|
42
|
+
/** Recursive item counts per folder id (an item counts toward every ancestor). */
|
|
43
|
+
export declare function recursiveItemCounts(folders: Record<string, MediaFolder>, items: MediaItem[]): Record<string, number>;
|
|
44
|
+
/** Pure heal: dangling folderIds dropped, orphan/cyclic folders re-rooted. */
|
|
45
|
+
export declare function healMediaManifest(manifest: MediaManifest): MediaManifest;
|
|
46
|
+
/**
|
|
47
|
+
* Save-time manifest assembly: merge uploaded pending items, apply deletions,
|
|
48
|
+
* preserve every other manifest key (folders!), then heal.
|
|
49
|
+
*/
|
|
50
|
+
export declare function assembleSavedManifest(manifest: MediaManifest, uploadedItems: MediaItem[], deletedIds: string[]): MediaManifest;
|
|
51
|
+
/**
|
|
52
|
+
* Map webkitRelativePath-style paths onto folder entities under
|
|
53
|
+
* `currentFolderId`, merging with existing folders by case-insensitive name.
|
|
54
|
+
*/
|
|
55
|
+
export declare function planFolderUpload(files: {
|
|
56
|
+
file: File;
|
|
57
|
+
relativePath: string;
|
|
58
|
+
}[], folders: Record<string, MediaFolder>, currentFolderId: string | null, newId?: () => string): FolderUploadPlan;
|
|
59
|
+
//# sourceMappingURL=media-folders.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"media-folders.d.ts","sourceRoot":"","sources":["../../src/lib/media-folders.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
export interface DroppedFile {
|
|
2
|
+
file: File;
|
|
3
|
+
relativePath: string;
|
|
4
|
+
}
|
|
5
|
+
/** Structural subset of FileSystemEntry — lets tests fake the tree. */
|
|
6
|
+
export interface EntryLike {
|
|
7
|
+
isFile: boolean;
|
|
8
|
+
isDirectory: boolean;
|
|
9
|
+
name: string;
|
|
10
|
+
fullPath: string;
|
|
11
|
+
file?: (cb: (f: File) => void, err: (e: unknown) => void) => void;
|
|
12
|
+
createReader?: () => {
|
|
13
|
+
readEntries: (cb: (entries: EntryLike[]) => void, err: (e: unknown) => void) => void;
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
export declare function traverseEntries(entries: EntryLike[]): Promise<DroppedFile[]>;
|
|
17
|
+
/**
|
|
18
|
+
* Dropped-payload traversal. Returns null when nothing in the drop is a
|
|
19
|
+
* directory — the caller should fall back to the plain flat-file path.
|
|
20
|
+
*/
|
|
21
|
+
export declare function collectDroppedFiles(dataTransfer: DataTransfer): Promise<DroppedFile[] | null>;
|
|
22
|
+
//# sourceMappingURL=folder-drop.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"folder-drop.d.ts","sourceRoot":"","sources":["../../src/media/folder-drop.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,WAAW;IAC1B,IAAI,EAAE,IAAI,CAAC;IACX,YAAY,EAAE,MAAM,CAAC;CACtB;AAED,uEAAuE;AACvE,MAAM,WAAW,SAAS;IACxB,MAAM,EAAE,OAAO,CAAC;IAChB,WAAW,EAAE,OAAO,CAAC;IACrB,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,EAAE,MAAM,CAAC;IACjB,IAAI,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC,EAAE,IAAI,KAAK,IAAI,EAAE,GAAG,EAAE,CAAC,CAAC,EAAE,OAAO,KAAK,IAAI,KAAK,IAAI,CAAC;IAClE,YAAY,CAAC,EAAE,MAAM;QACnB,WAAW,EAAE,CAAC,EAAE,EAAE,CAAC,OAAO,EAAE,SAAS,EAAE,KAAK,IAAI,EAAE,GAAG,EAAE,CAAC,CAAC,EAAE,OAAO,KAAK,IAAI,KAAK,IAAI,CAAC;KACtF,CAAC;CACH;AAoBD,wBAAsB,eAAe,CAAC,OAAO,EAAE,SAAS,EAAE,GAAG,OAAO,CAAC,WAAW,EAAE,CAAC,CAWlF;AAED;;;GAGG;AACH,wBAAsB,mBAAmB,CAAC,YAAY,EAAE,YAAY,GAAG,OAAO,CAAC,WAAW,EAAE,GAAG,IAAI,CAAC,CASnG"}
|
package/dist/media/index.js
CHANGED
|
@@ -5,6 +5,7 @@ import {
|
|
|
5
5
|
ProcessingQueue,
|
|
6
6
|
displayFilename,
|
|
7
7
|
displayFilenameExt,
|
|
8
|
+
displayName,
|
|
8
9
|
formatFileSize,
|
|
9
10
|
generateVideoPoster,
|
|
10
11
|
getMediaProvider,
|
|
@@ -15,14 +16,15 @@ import {
|
|
|
15
16
|
resolveMedia,
|
|
16
17
|
sanitizeMediaName,
|
|
17
18
|
setMediaProvider
|
|
18
|
-
} from "../chunk-
|
|
19
|
+
} from "../chunk-3LNKOLZP.js";
|
|
19
20
|
import "../chunk-4YEOTFVU.js";
|
|
20
21
|
import {
|
|
21
22
|
ImageManifestSchema,
|
|
22
23
|
MediaConfigSchema,
|
|
24
|
+
MediaFolderSchema,
|
|
23
25
|
MediaItemSchema,
|
|
24
26
|
VariantSchema
|
|
25
|
-
} from "../chunk-
|
|
27
|
+
} from "../chunk-4SIWBWKZ.js";
|
|
26
28
|
import "../chunk-JSBRDJBE.js";
|
|
27
29
|
export {
|
|
28
30
|
BAKED_MEDIA_KEYS,
|
|
@@ -30,11 +32,13 @@ export {
|
|
|
30
32
|
ImageManifestSchema,
|
|
31
33
|
MIME_TO_EXT,
|
|
32
34
|
MediaConfigSchema,
|
|
35
|
+
MediaFolderSchema,
|
|
33
36
|
MediaItemSchema,
|
|
34
37
|
ProcessingQueue,
|
|
35
38
|
VariantSchema,
|
|
36
39
|
displayFilename,
|
|
37
40
|
displayFilenameExt,
|
|
41
|
+
displayName,
|
|
38
42
|
formatFileSize,
|
|
39
43
|
generateVideoPoster,
|
|
40
44
|
getMediaProvider,
|
package/dist/media/types.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export { VariantSchema, MediaItemSchema, ImageManifestSchema, MediaConfigSchema, type Variant, type MediaItem, type MediaManifest, type MediaConfig, } from "../schemas/media";
|
|
1
|
+
export { VariantSchema, MediaItemSchema, ImageManifestSchema, MediaConfigSchema, MediaFolderSchema, type Variant, type MediaItem, type MediaManifest, type MediaConfig, type MediaFolder, } from "../schemas/media";
|
|
2
2
|
export type MediaKind = "image" | "animated" | "video";
|
|
3
3
|
export interface MediaFile {
|
|
4
4
|
buffer: ArrayBuffer;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/media/types.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,aAAa,EACb,eAAe,EACf,mBAAmB,EACnB,iBAAiB,EACjB,KAAK,OAAO,EACZ,KAAK,SAAS,EACd,KAAK,aAAa,EAClB,KAAK,WAAW,GACjB,MAAM,kBAAkB,CAAC;AAI1B,MAAM,MAAM,SAAS,GAAG,OAAO,GAAG,UAAU,GAAG,OAAO,CAAC;AAEvD,MAAM,WAAW,SAAS;IACxB,MAAM,EAAE,WAAW,CAAC;IACpB,YAAY,EAAE,MAAM,CAAC;IACrB,QAAQ,EAAE,MAAM,CAAC;CAClB;AAED,MAAM,WAAW,aAAa;IAC5B,GAAG,EAAE,KAAK,CAAC;IACX,GAAG,EAAE,MAAM,CAAC;IACZ,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,MAAM,CAAC;CAChB;AAED,MAAM,WAAW,gBAAgB;IAC/B,GAAG,EAAE,KAAK,CAAC;IACX,GAAG,EAAE,MAAM,CAAC;IACZ,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,MAAM,CAAC;CAChB;AAED,MAAM,WAAW,aAAa;IAC5B,GAAG,EAAE,OAAO,CAAC;IACb,GAAG,EAAE,MAAM,CAAC;IACZ,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,OAAO,CAAC;IAClB,IAAI,EAAE,OAAO,CAAC;IACd,KAAK,EAAE,OAAO,CAAC;CAChB;AAED,MAAM,MAAM,aAAa,GAAG,aAAa,GAAG,gBAAgB,GAAG,aAAa,CAAC"}
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/media/types.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,aAAa,EACb,eAAe,EACf,mBAAmB,EACnB,iBAAiB,EACjB,iBAAiB,EACjB,KAAK,OAAO,EACZ,KAAK,SAAS,EACd,KAAK,aAAa,EAClB,KAAK,WAAW,EAChB,KAAK,WAAW,GACjB,MAAM,kBAAkB,CAAC;AAI1B,MAAM,MAAM,SAAS,GAAG,OAAO,GAAG,UAAU,GAAG,OAAO,CAAC;AAEvD,MAAM,WAAW,SAAS;IACxB,MAAM,EAAE,WAAW,CAAC;IACpB,YAAY,EAAE,MAAM,CAAC;IACrB,QAAQ,EAAE,MAAM,CAAC;CAClB;AAED,MAAM,WAAW,aAAa;IAC5B,GAAG,EAAE,KAAK,CAAC;IACX,GAAG,EAAE,MAAM,CAAC;IACZ,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,MAAM,CAAC;CAChB;AAED,MAAM,WAAW,gBAAgB;IAC/B,GAAG,EAAE,KAAK,CAAC;IACX,GAAG,EAAE,MAAM,CAAC;IACZ,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,MAAM,CAAC;CAChB;AAED,MAAM,WAAW,aAAa;IAC5B,GAAG,EAAE,OAAO,CAAC;IACb,GAAG,EAAE,MAAM,CAAC;IACZ,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,OAAO,CAAC;IAClB,IAAI,EAAE,OAAO,CAAC;IACd,KAAK,EAAE,OAAO,CAAC;CAChB;AAED,MAAM,MAAM,aAAa,GAAG,aAAa,GAAG,gBAAgB,GAAG,aAAa,CAAC"}
|
package/dist/media/utils.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { MediaItem } from "./types";
|
|
1
2
|
export declare function hashFileBuffer(buffer: ArrayBuffer): Promise<string>;
|
|
2
3
|
export declare function sanitizeMediaName(name: string): string;
|
|
3
4
|
export declare const MIME_TO_EXT: Record<string, string>;
|
|
@@ -6,4 +7,10 @@ export declare function mimeToExt(mime: string): string;
|
|
|
6
7
|
export declare function displayFilenameExt(mime: string): string;
|
|
7
8
|
export declare function displayFilename(originalName: string, mimeType: string): string;
|
|
8
9
|
export declare function formatFileSize(bytes: number): string;
|
|
10
|
+
/**
|
|
11
|
+
* Display name for a media item. v2 batch-rename seam: will read a future
|
|
12
|
+
* optional `displayName` field with originalName fallback — UI must call this
|
|
13
|
+
* instead of touching originalName directly.
|
|
14
|
+
*/
|
|
15
|
+
export declare function displayName(item: Pick<MediaItem, "originalName">): string;
|
|
9
16
|
//# sourceMappingURL=utils.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"utils.d.ts","sourceRoot":"","sources":["../../src/media/utils.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"utils.d.ts","sourceRoot":"","sources":["../../src/media/utils.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,SAAS,CAAC;AAEzC,wBAAsB,cAAc,CAAC,MAAM,EAAE,WAAW,GAAG,OAAO,CAAC,MAAM,CAAC,CAqBzE;AAKD,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAEtD;AAED,eAAO,MAAM,WAAW,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAI9C,CAAC;AAEF,eAAO,MAAM,WAAW,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAK9C,CAAC;AAEF,wBAAgB,SAAS,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAE9C;AAED,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAGvD;AAED,wBAAgB,eAAe,CAAC,YAAY,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,MAAM,CAM9E;AAED,wBAAgB,cAAc,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAIpD;AAED;;;;GAIG;AACH,wBAAgB,WAAW,CAAC,IAAI,EAAE,IAAI,CAAC,SAAS,EAAE,cAAc,CAAC,GAAG,MAAM,CAEzE"}
|
package/dist/schemas/index.js
CHANGED
|
@@ -3,7 +3,7 @@ import {
|
|
|
3
3
|
AudienceNameSchema,
|
|
4
4
|
MediaGridOptionsSchema,
|
|
5
5
|
slugifyAudienceName
|
|
6
|
-
} from "../chunk-
|
|
6
|
+
} from "../chunk-MK4POMLZ.js";
|
|
7
7
|
import {
|
|
8
8
|
AudienceSchema,
|
|
9
9
|
RoleSchema,
|
|
@@ -28,6 +28,7 @@ import {
|
|
|
28
28
|
ImageManifestSchema,
|
|
29
29
|
IndexSchema,
|
|
30
30
|
MediaConfigSchema,
|
|
31
|
+
MediaFolderSchema,
|
|
31
32
|
MediaItemSchema,
|
|
32
33
|
MediaReferenceSchema,
|
|
33
34
|
PageSchema,
|
|
@@ -39,7 +40,7 @@ import {
|
|
|
39
40
|
TextLineSchema,
|
|
40
41
|
VariantSchema,
|
|
41
42
|
normalizeSiteIndex
|
|
42
|
-
} from "../chunk-
|
|
43
|
+
} from "../chunk-4SIWBWKZ.js";
|
|
43
44
|
import "../chunk-VKAGMEKE.js";
|
|
44
45
|
import "../chunk-NSCT3AMV.js";
|
|
45
46
|
import "../chunk-JSBRDJBE.js";
|
|
@@ -57,6 +58,7 @@ export {
|
|
|
57
58
|
LinkTargetSchema,
|
|
58
59
|
LinkValueSchema,
|
|
59
60
|
MediaConfigSchema,
|
|
61
|
+
MediaFolderSchema,
|
|
60
62
|
MediaGridOptionsSchema,
|
|
61
63
|
MediaItemSchema,
|
|
62
64
|
MediaReferenceSchema,
|
package/dist/schemas/media.d.ts
CHANGED
|
@@ -5,6 +5,11 @@ export declare const VariantSchema: z.ZodObject<{
|
|
|
5
5
|
size: z.ZodNumber;
|
|
6
6
|
}, z.core.$strip>;
|
|
7
7
|
export type Variant = z.infer<typeof VariantSchema>;
|
|
8
|
+
export declare const MediaFolderSchema: z.ZodObject<{
|
|
9
|
+
name: z.ZodString;
|
|
10
|
+
parentId: z.ZodNullable<z.ZodString>;
|
|
11
|
+
}, z.core.$strip>;
|
|
12
|
+
export type MediaFolder = z.infer<typeof MediaFolderSchema>;
|
|
8
13
|
export declare const MediaItemSchema: z.ZodObject<{
|
|
9
14
|
id: z.ZodString;
|
|
10
15
|
hash: z.ZodString;
|
|
@@ -31,6 +36,7 @@ export declare const MediaItemSchema: z.ZodObject<{
|
|
|
31
36
|
}>>;
|
|
32
37
|
url: z.ZodOptional<z.ZodString>;
|
|
33
38
|
duration: z.ZodOptional<z.ZodNumber>;
|
|
39
|
+
folderId: z.ZodOptional<z.ZodString>;
|
|
34
40
|
}, z.core.$strip>;
|
|
35
41
|
export type MediaItem = z.infer<typeof MediaItemSchema>;
|
|
36
42
|
export declare const ImageManifestSchema: z.ZodObject<{
|
|
@@ -60,7 +66,12 @@ export declare const ImageManifestSchema: z.ZodObject<{
|
|
|
60
66
|
}>>;
|
|
61
67
|
url: z.ZodOptional<z.ZodString>;
|
|
62
68
|
duration: z.ZodOptional<z.ZodNumber>;
|
|
69
|
+
folderId: z.ZodOptional<z.ZodString>;
|
|
63
70
|
}, z.core.$strip>>;
|
|
71
|
+
folders: z.ZodDefault<z.ZodRecord<z.ZodString, z.ZodObject<{
|
|
72
|
+
name: z.ZodString;
|
|
73
|
+
parentId: z.ZodNullable<z.ZodString>;
|
|
74
|
+
}, z.core.$strip>>>;
|
|
64
75
|
}, z.core.$strip>;
|
|
65
76
|
export type MediaManifest = z.infer<typeof ImageManifestSchema>;
|
|
66
77
|
export declare const MediaConfigSchema: z.ZodObject<{
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"media.d.ts","sourceRoot":"","sources":["../../src/schemas/media.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAExB,eAAO,MAAM,aAAa;;;;iBAIxB,CAAC;AAEH,MAAM,MAAM,OAAO,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,aAAa,CAAC,CAAC;
|
|
1
|
+
{"version":3,"file":"media.d.ts","sourceRoot":"","sources":["../../src/schemas/media.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAExB,eAAO,MAAM,aAAa;;;;iBAIxB,CAAC;AAEH,MAAM,MAAM,OAAO,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,aAAa,CAAC,CAAC;AAIpD,eAAO,MAAM,iBAAiB;;;iBAG5B,CAAC;AAEH,MAAM,MAAM,WAAW,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,iBAAiB,CAAC,CAAC;AAE5D,eAAO,MAAM,eAAe;;;;;;;;;;;;;;;;;;;;;;;;;;;iBAwB1B,CAAC;AAEH,MAAM,MAAM,SAAS,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,eAAe,CAAC,CAAC;AAExD,eAAO,MAAM,mBAAmB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;iBAG9B,CAAC;AAEH,MAAM,MAAM,aAAa,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,mBAAmB,CAAC,CAAC;AAEhE,eAAO,MAAM,iBAAiB;;;;;iBAO5B,CAAC;AAEH,MAAM,MAAM,WAAW,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,iBAAiB,CAAC,CAAC"}
|
package/package.json
CHANGED
|
@@ -158,7 +158,7 @@ export default function EditorShell({
|
|
|
158
158
|
const [showSiteSettings, setShowSiteSettings] = useState(false);
|
|
159
159
|
const [dirtySectionIds, setDirtySectionIds] = useState<Set<string>>(new Set());
|
|
160
160
|
const [siteConfig, setSiteConfig] = useState<SiteConfig | null>(null);
|
|
161
|
-
const [mediaManifest, setMediaManifest] = useState<MediaManifest>({ images: {} });
|
|
161
|
+
const [mediaManifest, setMediaManifest] = useState<MediaManifest>({ images: {}, folders: {} });
|
|
162
162
|
const [showMediaLibrary, setShowMediaLibrary] = useState(false);
|
|
163
163
|
const [mediaModalMode, setMediaModalMode] = useState<"select" | "manage">("manage");
|
|
164
164
|
const [mediaSelectCallback, setMediaSelectCallback] = useState<((id: string) => void) | null>(null);
|
|
@@ -192,6 +192,9 @@ export default function EditorShell({
|
|
|
192
192
|
// upload-intent failures, direct-to-bucket PUT failures) in the media
|
|
193
193
|
// library modal, alongside its existing client-side rejection banner.
|
|
194
194
|
const [uploadErrorMessage, setUploadErrorMessage] = useState<string | null>(null);
|
|
195
|
+
// Surfaces useMediaPipeline's onUploadNotice (e.g. "N files already exist
|
|
196
|
+
// and were left where they are" on dedup skip) in the media library modal.
|
|
197
|
+
const [uploadNotice, setUploadNotice] = useState<string | null>(null);
|
|
195
198
|
const [activePageId, setActivePageId] = useState<string | null>(null);
|
|
196
199
|
const [movingSectionId, setMovingSectionId] = useState<string | null>(null);
|
|
197
200
|
// Set by recoverConflict when a rebase surfaces overlapping edits; drives the
|
|
@@ -317,6 +320,7 @@ export default function EditorShell({
|
|
|
317
320
|
setDirtySectionIds,
|
|
318
321
|
markSectionDirty: persistence.markSectionDirty,
|
|
319
322
|
onUploadError: (message) => setUploadErrorMessage(message),
|
|
323
|
+
onUploadNotice: setUploadNotice,
|
|
320
324
|
});
|
|
321
325
|
|
|
322
326
|
const buildStatus = useBuildStatus();
|
|
@@ -737,7 +741,7 @@ export default function EditorShell({
|
|
|
737
741
|
for (const id of publishedDeletions) {
|
|
738
742
|
delete images[id];
|
|
739
743
|
}
|
|
740
|
-
return { images };
|
|
744
|
+
return { ...prev, images };
|
|
741
745
|
});
|
|
742
746
|
},
|
|
743
747
|
onShasUpdated: (newSavedSha, newMainSha) => {
|
|
@@ -866,7 +870,7 @@ export default function EditorShell({
|
|
|
866
870
|
let loadedSections: LoadedSection[];
|
|
867
871
|
let loadedIndex: SiteIndex;
|
|
868
872
|
let loadedConfig: SiteConfig;
|
|
869
|
-
let loadedManifest: MediaManifest = { images: {} };
|
|
873
|
+
let loadedManifest: MediaManifest = { images: {}, folders: {} };
|
|
870
874
|
|
|
871
875
|
// Key the content cache on the head of the branch we actually load (the
|
|
872
876
|
// draft `saved` branch if it exists, else main), provided by the SSR edit
|
|
@@ -1056,7 +1060,7 @@ export default function EditorShell({
|
|
|
1056
1060
|
}
|
|
1057
1061
|
}
|
|
1058
1062
|
const savedManifest = await getMediaManifest();
|
|
1059
|
-
setMediaManifest(savedManifest ?? { images: {} });
|
|
1063
|
+
setMediaManifest(savedManifest ?? { images: {}, folders: {} });
|
|
1060
1064
|
persistence.clearConfigDirty();
|
|
1061
1065
|
// Discard wiped the Dexie siteIndex row — no unsaved structure remains.
|
|
1062
1066
|
// Tokenless: discard is unconditional, unlike the save-success clear.
|
|
@@ -1522,6 +1526,9 @@ export default function EditorShell({
|
|
|
1522
1526
|
}}
|
|
1523
1527
|
title="Media Library"
|
|
1524
1528
|
size="large"
|
|
1529
|
+
// Padding lives on the two scroll columns inside MediaLibraryModal
|
|
1530
|
+
// so their scrollbars sit at the pane edges.
|
|
1531
|
+
noPadding
|
|
1525
1532
|
>
|
|
1526
1533
|
<MediaLibraryModal
|
|
1527
1534
|
mode={mediaModalMode}
|
|
@@ -1545,6 +1552,15 @@ export default function EditorShell({
|
|
|
1545
1552
|
uploadProgress={mediaPipeline.uploadProgress}
|
|
1546
1553
|
uploadError={uploadErrorMessage}
|
|
1547
1554
|
onDismissUploadError={() => setUploadErrorMessage(null)}
|
|
1555
|
+
folders={mediaManifest.folders}
|
|
1556
|
+
onCreateFolder={mediaPipeline.handleFolderCreate}
|
|
1557
|
+
onRenameFolder={mediaPipeline.handleFolderRename}
|
|
1558
|
+
onMoveFolder={mediaPipeline.handleFolderMove}
|
|
1559
|
+
onDeleteFolder={mediaPipeline.handleFolderDelete}
|
|
1560
|
+
onMoveMedia={mediaPipeline.handleMediaMove}
|
|
1561
|
+
onFolderUpload={mediaPipeline.handleFolderUpload}
|
|
1562
|
+
uploadNotice={uploadNotice}
|
|
1563
|
+
onDismissUploadNotice={() => setUploadNotice(null)}
|
|
1548
1564
|
/>
|
|
1549
1565
|
</EditorModal>
|
|
1550
1566
|
<EditorModal
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { Fragment, useRef } from "react";
|
|
2
|
+
import { ChevronRight } from "lucide-react";
|
|
3
|
+
import { cn } from "../../lib/cn";
|
|
4
|
+
import type { MediaFolder, MediaItem } from "../../media/types";
|
|
5
|
+
import { folderPathIds } from "../../lib/media-folders";
|
|
6
|
+
import { useFolderDropTarget } from "../../hooks/useMediaFolderDnd";
|
|
7
|
+
import type { MediaFolderDndBindings } from "./MediaFolderTree";
|
|
8
|
+
|
|
9
|
+
export interface MediaBreadcrumbProps {
|
|
10
|
+
folders: Record<string, MediaFolder>;
|
|
11
|
+
currentFolderId: string | null;
|
|
12
|
+
onNavigate: (id: string | null) => void;
|
|
13
|
+
dnd?: MediaFolderDndBindings;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
function Segment({
|
|
18
|
+
label, target, onNavigate, dnd, isCurrent,
|
|
19
|
+
}: {
|
|
20
|
+
label: string;
|
|
21
|
+
target: string | null;
|
|
22
|
+
onNavigate: (id: string | null) => void;
|
|
23
|
+
dnd?: MediaFolderDndBindings;
|
|
24
|
+
isCurrent: boolean;
|
|
25
|
+
}) {
|
|
26
|
+
const emptyFoldersRef = useRef<Record<string, MediaFolder>>({});
|
|
27
|
+
const emptyItemsRef = useRef<Record<string, MediaItem>>({});
|
|
28
|
+
const { ref, isDropTarget } = useFolderDropTarget<HTMLButtonElement>({
|
|
29
|
+
targetFolderId: target,
|
|
30
|
+
enabled: dnd !== undefined && !isCurrent,
|
|
31
|
+
foldersRef: dnd?.foldersRef ?? emptyFoldersRef,
|
|
32
|
+
itemsByIdRef: dnd?.itemsByIdRef ?? emptyItemsRef,
|
|
33
|
+
onMoveMedia: dnd?.onMoveMedia ?? (() => {}),
|
|
34
|
+
onMoveFolder: dnd?.onMoveFolder ?? (() => {}),
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
return (
|
|
38
|
+
<button
|
|
39
|
+
ref={ref}
|
|
40
|
+
type="button"
|
|
41
|
+
disabled={isCurrent}
|
|
42
|
+
onClick={() => onNavigate(target)}
|
|
43
|
+
className={cn(
|
|
44
|
+
"max-w-40 truncate rounded px-1 py-0.5 text-sm",
|
|
45
|
+
isCurrent
|
|
46
|
+
? "font-medium text-base-contrast"
|
|
47
|
+
: "cursor-pointer text-base-contrast-light hover:text-base-contrast",
|
|
48
|
+
isDropTarget && "ring-2 ring-primary ring-inset",
|
|
49
|
+
)}
|
|
50
|
+
>
|
|
51
|
+
{label}
|
|
52
|
+
</button>
|
|
53
|
+
);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export function MediaBreadcrumb({ folders, currentFolderId, onNavigate, dnd }: MediaBreadcrumbProps) {
|
|
57
|
+
if (currentFolderId === null) return null;
|
|
58
|
+
const pathIds = folderPathIds(folders, currentFolderId);
|
|
59
|
+
|
|
60
|
+
return (
|
|
61
|
+
<div className="flex flex-wrap items-center gap-0.5">
|
|
62
|
+
<Segment label="All media" target={null} onNavigate={onNavigate} dnd={dnd} isCurrent={false} />
|
|
63
|
+
{pathIds.map((id, i) => (
|
|
64
|
+
<Fragment key={id}>
|
|
65
|
+
<ChevronRight size={12} className="shrink-0 text-base-contrast-light" />
|
|
66
|
+
<Segment
|
|
67
|
+
label={folders[id].name}
|
|
68
|
+
target={id}
|
|
69
|
+
onNavigate={onNavigate}
|
|
70
|
+
dnd={dnd}
|
|
71
|
+
isCurrent={i === pathIds.length - 1}
|
|
72
|
+
/>
|
|
73
|
+
</Fragment>
|
|
74
|
+
))}
|
|
75
|
+
</div>
|
|
76
|
+
);
|
|
77
|
+
}
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
import { useState, useRef, useEffect } from "react";
|
|
2
|
+
import { Folder, MoreVertical } from "lucide-react";
|
|
3
|
+
import { cn } from "../../lib/cn";
|
|
4
|
+
import { buildFolderDragData } from "../../lib/media-dnd";
|
|
5
|
+
import type { MediaFolder, MediaItem } from "../../media/types";
|
|
6
|
+
import { useFolderDropTarget, useMediaDraggable } from "../../hooks/useMediaFolderDnd";
|
|
7
|
+
import type { MediaFolderDndBindings } from "./MediaFolderTree";
|
|
8
|
+
|
|
9
|
+
export interface MediaFolderTileProps {
|
|
10
|
+
id: string;
|
|
11
|
+
name: string;
|
|
12
|
+
count: number;
|
|
13
|
+
onOpen: (id: string) => void;
|
|
14
|
+
manage: boolean;
|
|
15
|
+
onRename?: (id: string, name: string) => void;
|
|
16
|
+
onDeleteRequest?: (id: string) => void;
|
|
17
|
+
dnd?: MediaFolderDndBindings;
|
|
18
|
+
/** Optional accessible label override for the root element (e.g. "Open folder Logos"). */
|
|
19
|
+
ariaLabel?: string;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
export function MediaFolderTile({
|
|
24
|
+
id, name, count, onOpen, manage, onRename, onDeleteRequest, dnd, ariaLabel,
|
|
25
|
+
}: MediaFolderTileProps) {
|
|
26
|
+
const [menuOpen, setMenuOpen] = useState(false);
|
|
27
|
+
const [renaming, setRenaming] = useState(false);
|
|
28
|
+
const [draft, setDraft] = useState(name);
|
|
29
|
+
const menuRef = useRef<HTMLDivElement>(null);
|
|
30
|
+
const emptyFoldersRef = useRef<Record<string, MediaFolder>>({});
|
|
31
|
+
const emptyItemsRef = useRef<Record<string, MediaItem>>({});
|
|
32
|
+
|
|
33
|
+
const drag = useMediaDraggable<HTMLDivElement>({
|
|
34
|
+
enabled: manage && dnd !== undefined && !renaming,
|
|
35
|
+
getDragData: () => buildFolderDragData(id),
|
|
36
|
+
});
|
|
37
|
+
const drop = useFolderDropTarget<HTMLDivElement>({
|
|
38
|
+
targetFolderId: id,
|
|
39
|
+
enabled: dnd !== undefined,
|
|
40
|
+
foldersRef: dnd?.foldersRef ?? emptyFoldersRef,
|
|
41
|
+
itemsByIdRef: dnd?.itemsByIdRef ?? emptyItemsRef,
|
|
42
|
+
onMoveMedia: dnd?.onMoveMedia ?? (() => {}),
|
|
43
|
+
onMoveFolder: dnd?.onMoveFolder ?? (() => {}),
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
// Dismiss the kebab menu on any pointerdown outside its container.
|
|
47
|
+
useEffect(() => {
|
|
48
|
+
if (!menuOpen) return;
|
|
49
|
+
const handler = (e: PointerEvent) => {
|
|
50
|
+
if (menuRef.current && e.target instanceof Node && !menuRef.current.contains(e.target)) {
|
|
51
|
+
setMenuOpen(false);
|
|
52
|
+
}
|
|
53
|
+
};
|
|
54
|
+
document.addEventListener("pointerdown", handler);
|
|
55
|
+
return () => document.removeEventListener("pointerdown", handler);
|
|
56
|
+
}, [menuOpen]);
|
|
57
|
+
|
|
58
|
+
const commitRename = () => {
|
|
59
|
+
setRenaming(false);
|
|
60
|
+
const next = draft.trim();
|
|
61
|
+
if (next && next !== name) onRename?.(id, next);
|
|
62
|
+
else setDraft(name);
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
return (
|
|
66
|
+
<div
|
|
67
|
+
// The tile is both a draggable and a drop target — a callback ref fills
|
|
68
|
+
// both hook refs at commit time, BEFORE either hook's registration
|
|
69
|
+
// effect runs (an effect-based sync would run after and never register).
|
|
70
|
+
ref={(el) => { drag.ref.current = el; drop.ref.current = el; }}
|
|
71
|
+
role="button"
|
|
72
|
+
tabIndex={0}
|
|
73
|
+
aria-label={ariaLabel}
|
|
74
|
+
onClick={() => { if (!renaming) onOpen(id); }}
|
|
75
|
+
onKeyDown={(e) => { if (e.key === "Enter" && !renaming) onOpen(id); }}
|
|
76
|
+
className={cn(
|
|
77
|
+
"group/tile relative flex items-center gap-2 rounded-lg border border-base-200 px-3 py-2.5 transition-colors",
|
|
78
|
+
"cursor-pointer hover:border-base-300 hover:bg-base-accent",
|
|
79
|
+
drag.isDragging && "opacity-40",
|
|
80
|
+
drop.isDropTarget && "border-primary ring-2 ring-primary ring-inset",
|
|
81
|
+
)}
|
|
82
|
+
>
|
|
83
|
+
<Folder size={16} className="shrink-0 text-base-contrast-light" />
|
|
84
|
+
{renaming ? (
|
|
85
|
+
<input
|
|
86
|
+
autoFocus
|
|
87
|
+
value={draft}
|
|
88
|
+
onChange={(e) => setDraft(e.target.value)}
|
|
89
|
+
onClick={(e) => e.stopPropagation()}
|
|
90
|
+
onKeyDown={(e) => {
|
|
91
|
+
if (e.key === "Enter") commitRename();
|
|
92
|
+
if (e.key === "Escape") { setRenaming(false); setDraft(name); }
|
|
93
|
+
}}
|
|
94
|
+
onBlur={commitRename}
|
|
95
|
+
className="min-w-0 flex-1 rounded border border-base-200 bg-base px-1 py-0.5 text-sm text-base-contrast focus:outline-none focus:ring-1 focus:ring-primary"
|
|
96
|
+
/>
|
|
97
|
+
) : (
|
|
98
|
+
<span className="min-w-0 flex-1 truncate text-sm text-base-contrast">{name}</span>
|
|
99
|
+
)}
|
|
100
|
+
{!renaming && count > 0 && (
|
|
101
|
+
<span className="shrink-0 text-xs text-base-contrast-light">{count}</span>
|
|
102
|
+
)}
|
|
103
|
+
{manage && !renaming && (
|
|
104
|
+
<div ref={menuRef} className="relative shrink-0">
|
|
105
|
+
<button
|
|
106
|
+
type="button"
|
|
107
|
+
aria-label={`Folder actions: ${name}`}
|
|
108
|
+
onClick={(e) => { e.stopPropagation(); setMenuOpen((v) => !v); }}
|
|
109
|
+
// Enter/Space keydown bubbles to the tile root BEFORE the button's
|
|
110
|
+
// synthetic click fires — stop it so the tile doesn't open (see
|
|
111
|
+
// the matching chevron fix in MediaFolderTree.tsx).
|
|
112
|
+
onKeyDown={(e) => { if (e.key === "Enter" || e.key === " ") e.stopPropagation(); }}
|
|
113
|
+
className="rounded p-1 opacity-0 transition-opacity hover:bg-base-200/50 focus:opacity-100 group-hover/tile:opacity-100"
|
|
114
|
+
>
|
|
115
|
+
<MoreVertical size={16} />
|
|
116
|
+
</button>
|
|
117
|
+
{menuOpen && (
|
|
118
|
+
<div className="absolute right-0 top-6 z-20 w-28 rounded-md border border-base-200 bg-base py-1 shadow-lg">
|
|
119
|
+
<button
|
|
120
|
+
type="button"
|
|
121
|
+
className="block w-full px-3 py-1.5 text-left text-sm text-base-contrast hover:bg-base-accent"
|
|
122
|
+
onClick={(e) => { e.stopPropagation(); setMenuOpen(false); setDraft(name); setRenaming(true); }}
|
|
123
|
+
onKeyDown={(e) => { if (e.key === "Enter" || e.key === " ") e.stopPropagation(); }}
|
|
124
|
+
>
|
|
125
|
+
Rename
|
|
126
|
+
</button>
|
|
127
|
+
<button
|
|
128
|
+
type="button"
|
|
129
|
+
className="block w-full px-3 py-1.5 text-left text-sm text-red-600 hover:bg-base-accent dark:text-red-400"
|
|
130
|
+
onClick={(e) => { e.stopPropagation(); setMenuOpen(false); onDeleteRequest?.(id); }}
|
|
131
|
+
onKeyDown={(e) => { if (e.key === "Enter" || e.key === " ") e.stopPropagation(); }}
|
|
132
|
+
>
|
|
133
|
+
Delete
|
|
134
|
+
</button>
|
|
135
|
+
</div>
|
|
136
|
+
)}
|
|
137
|
+
</div>
|
|
138
|
+
)}
|
|
139
|
+
</div>
|
|
140
|
+
);
|
|
141
|
+
}
|