@orthacms/media-admin 0.10.2 → 0.11.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (26) hide show
  1. package/dist/lib/components/FadeInImage/index.d.ts +24 -0
  2. package/dist/lib/components/FadeInImage/index.d.ts.map +1 -0
  3. package/dist/lib/components/FadeInImage/index.js +34 -0
  4. package/dist/lib/components/MediaFieldControl/MediaFieldItem/index.d.ts.map +1 -1
  5. package/dist/lib/components/MediaFieldControl/MediaFieldItem/index.js +2 -1
  6. package/dist/lib/components/MediaGrid/MediaAssetCard/index.d.ts +7 -1
  7. package/dist/lib/components/MediaGrid/MediaAssetCard/index.d.ts.map +1 -1
  8. package/dist/lib/components/MediaGrid/MediaAssetCard/index.js +2 -2
  9. package/dist/lib/components/MediaGrid/index.d.ts.map +1 -1
  10. package/dist/lib/components/MediaGrid/index.js +4 -1
  11. package/dist/lib/components/MediaThumbnail/index.d.ts +3 -2
  12. package/dist/lib/components/MediaThumbnail/index.d.ts.map +1 -1
  13. package/dist/lib/components/MediaThumbnail/index.js +5 -3
  14. package/dist/lib/components/UploadDialog/StagedFileRow/index.d.ts.map +1 -1
  15. package/dist/lib/components/UploadDialog/StagedFileRow/index.js +2 -1
  16. package/dist/lib/constants/index.d.ts +13 -0
  17. package/dist/lib/constants/index.d.ts.map +1 -1
  18. package/dist/lib/constants/index.js +13 -0
  19. package/dist/lib/hooks/useUploadQueue/index.d.ts +2 -0
  20. package/dist/lib/hooks/useUploadQueue/index.d.ts.map +1 -1
  21. package/dist/lib/hooks/useUploadQueue/index.js +15 -1
  22. package/dist/lib/pages/MediaLibraryPage/index.d.ts.map +1 -1
  23. package/dist/lib/pages/MediaLibraryPage/index.js +6 -4
  24. package/dist/lib/utils/mediaPlugin/index.d.ts.map +1 -1
  25. package/dist/lib/utils/mediaPlugin/index.js +3 -4
  26. package/package.json +10 -10
@@ -0,0 +1,24 @@
1
+ import type { ImgHTMLAttributes } from 'react';
2
+ /** Props for {@link FadeInImage} — a plain `<img>`'s, `src` required. */
3
+ export type FadeInImageProps = ImgHTMLAttributes<HTMLImageElement> & {
4
+ src: string;
5
+ };
6
+ /**
7
+ * An `<img>` that fades in once its bytes have arrived, instead of painting
8
+ * top-down over the tile. Whatever sits behind it (the tile's gradient or
9
+ * muted fill) is the placeholder, so nothing shifts when it lands.
10
+ *
11
+ * Two cases would otherwise leave it invisible for good:
12
+ *
13
+ * - **A cached image** can finish before React attaches `onLoad`, so the event
14
+ * never reaches us. It is already `complete` on mount, which the layout
15
+ * effect reads before the first paint — no fade, no flash.
16
+ * - **A broken image** never fires `load`. `error` reveals it too: the alt text
17
+ * (and the browser's broken-image glyph) is information, an empty tile is not.
18
+ *
19
+ * Visibility is keyed on the `src` that settled, so a tile reused for a
20
+ * different asset starts hidden again rather than showing the new image
21
+ * half-drawn. Reduced motion skips the fade, not the image.
22
+ */
23
+ export declare function FadeInImage({ src, className, onLoad, onError, alt, ...props }: FadeInImageProps): import("react").JSX.Element;
24
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../src/lib/components/FadeInImage/index.tsx"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,iBAAiB,EAAkB,MAAM,OAAO,CAAC;AAG/D,yEAAyE;AACzE,MAAM,MAAM,gBAAgB,GAAG,iBAAiB,CAAC,gBAAgB,CAAC,GAAG;IACjE,GAAG,EAAE,MAAM,CAAC;CACf,CAAC;AAEF;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAgB,WAAW,CAAC,EACxB,GAAG,EACH,SAAS,EACT,MAAM,EACN,OAAO,EACP,GAAG,EACH,GAAG,KAAK,EACX,EAAE,gBAAgB,+BAiClB"}
@@ -0,0 +1,34 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { useLayoutEffect, useRef, useState } from 'react';
3
+ import { cn } from '@orthacms/design-system';
4
+ /**
5
+ * An `<img>` that fades in once its bytes have arrived, instead of painting
6
+ * top-down over the tile. Whatever sits behind it (the tile's gradient or
7
+ * muted fill) is the placeholder, so nothing shifts when it lands.
8
+ *
9
+ * Two cases would otherwise leave it invisible for good:
10
+ *
11
+ * - **A cached image** can finish before React attaches `onLoad`, so the event
12
+ * never reaches us. It is already `complete` on mount, which the layout
13
+ * effect reads before the first paint — no fade, no flash.
14
+ * - **A broken image** never fires `load`. `error` reveals it too: the alt text
15
+ * (and the browser's broken-image glyph) is information, an empty tile is not.
16
+ *
17
+ * Visibility is keyed on the `src` that settled, so a tile reused for a
18
+ * different asset starts hidden again rather than showing the new image
19
+ * half-drawn. Reduced motion skips the fade, not the image.
20
+ */
21
+ export function FadeInImage({ src, className, onLoad, onError, alt, ...props }) {
22
+ const ref = useRef(null);
23
+ const [settledSrc, setSettledSrc] = useState(null);
24
+ useLayoutEffect(() => {
25
+ if (ref.current?.complete)
26
+ setSettledSrc(src);
27
+ }, [src]);
28
+ const settle = (handler) => (event) => {
29
+ setSettledSrc(src);
30
+ handler?.(event);
31
+ };
32
+ const visible = settledSrc === src;
33
+ return (_jsx("img", { ...props, ref: ref, src: src, alt: alt, onLoad: settle(onLoad), onError: settle(onError), "data-loaded": visible ? 'true' : 'false', className: cn('transition-opacity duration-(--ds-duration-base) ease-(--ds-ease-out) motion-reduce:transition-none', visible ? 'opacity-100' : 'opacity-0', className) }));
34
+ }
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../../src/lib/components/MediaFieldControl/MediaFieldItem/index.tsx"],"names":[],"mappings":"AAmBA,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,kCAAkC,CAAC;AAoE1E;;;;;;;;;;;;;GAaG;AACH,wBAAgB,cAAc,CAAC,EAC3B,IAAI,EACJ,KAAK,EACL,KAAK,EACL,QAAQ,EACR,QAAgB,EAChB,QAAQ,EACR,MAAM,EACN,GAAG,EACH,UAAkB,EAClB,WAAW,EACX,kBAAkB,EACrB,EAAE;IACC,IAAI,EAAE,iBAAiB,CAAC;IACxB,wEAAwE;IACxE,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,MAAM,CAAC;IACd,2EAA2E;IAC3E,QAAQ,EAAE,OAAO,CAAC;IAClB,2DAA2D;IAC3D,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,QAAQ,EAAE,MAAM,IAAI,CAAC;IACrB,MAAM,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;IAChC,6DAA6D;IAC7D,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,0DAA0D;IAC1D,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,WAAW,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;IACrC,kBAAkB,CAAC,EAAE,CAAC,IAAI,EAAE,OAAO,KAAK,IAAI,CAAC;CAChD,+BAsOA"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../../src/lib/components/MediaFieldControl/MediaFieldItem/index.tsx"],"names":[],"mappings":"AAmBA,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,kCAAkC,CAAC;AAqE1E;;;;;;;;;;;;;GAaG;AACH,wBAAgB,cAAc,CAAC,EAC3B,IAAI,EACJ,KAAK,EACL,KAAK,EACL,QAAQ,EACR,QAAgB,EAChB,QAAQ,EACR,MAAM,EACN,GAAG,EACH,UAAkB,EAClB,WAAW,EACX,kBAAkB,EACrB,EAAE;IACC,IAAI,EAAE,iBAAiB,CAAC;IACxB,wEAAwE;IACxE,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,MAAM,CAAC;IACd,2EAA2E;IAC3E,QAAQ,EAAE,OAAO,CAAC;IAClB,2DAA2D;IAC3D,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,QAAQ,EAAE,MAAM,IAAI,CAAC;IACrB,MAAM,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;IAChC,6DAA6D;IAC7D,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,0DAA0D;IAC1D,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,WAAW,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;IACrC,kBAAkB,CAAC,EAAE,CAAC,IAAI,EAAE,OAAO,KAAK,IAAI,CAAC;CAChD,+BAsOA"}
@@ -7,6 +7,7 @@ import { MEDIA_ALT_MAX_LENGTH } from '@orthacms/content-domain';
7
7
  import { MEDIA_KIND } from '../../../constants';
8
8
  import { assetGradient } from '../../../utils/assetGradient';
9
9
  import { formatBytes } from '../../../utils/formatBytes';
10
+ import { FadeInImage } from '../../FadeInImage';
10
11
  import { MediaKindIcon } from '../../MediaKindIcon';
11
12
  /** Intl descriptors for {@link MediaFieldItem}, co-located. */
12
13
  const messages = defineMessages({
@@ -98,7 +99,7 @@ export function MediaFieldItem({ item, index, total, multiple, readOnly = false,
98
99
  .join(' · ');
99
100
  return (_jsxs("li", { className: cn('group/item relative overflow-hidden rounded-lg border bg-card transition-shadow hover:shadow-md', item.missing && 'border-destructive/50', (item.pending || item.resolving) && 'border-dashed'), children: [_jsxs("div", { className: "relative aspect-[4/3] w-full overflow-hidden bg-muted", style: !item.missing && !item.resolving && !asImage
100
101
  ? { backgroundImage: assetGradient(item.id) }
101
- : undefined, children: [item.resolving ? (_jsx("span", { className: "flex size-full animate-pulse items-center justify-center bg-muted text-[11px] text-muted-foreground", children: intl.formatMessage(messages.resolving) })) : item.missing ? (_jsxs("span", { className: "flex size-full flex-col items-center justify-center gap-1 bg-destructive/5 px-2 text-center text-destructive", children: [_jsx(FileWarning, { className: "size-6", "aria-hidden": true }), _jsx("span", { className: "text-[11px] font-medium leading-tight", children: intl.formatMessage(messages.unavailableHint) })] })) : asImage ? (_jsx("img", { src: item.url, alt: item.name, loading: "lazy", className: "absolute inset-0 size-full object-cover" })) : (_jsx("span", { className: "flex size-full items-center justify-center", children: _jsx(MediaKindIcon, { kind: kindOf(item.kind), className: "size-8 text-white/90 drop-shadow-sm" }) })), multiple ? (_jsx("span", { className: "absolute left-1.5 top-1.5 grid size-5 place-items-center rounded bg-black/60 text-[11px] font-medium tabular-nums text-white", title: intl.formatMessage(messages.position, {
102
+ : undefined, children: [item.resolving ? (_jsx("span", { className: "flex size-full animate-pulse items-center justify-center bg-muted text-[11px] text-muted-foreground", children: intl.formatMessage(messages.resolving) })) : item.missing ? (_jsxs("span", { className: "flex size-full flex-col items-center justify-center gap-1 bg-destructive/5 px-2 text-center text-destructive", children: [_jsx(FileWarning, { className: "size-6", "aria-hidden": true }), _jsx("span", { className: "text-[11px] font-medium leading-tight", children: intl.formatMessage(messages.unavailableHint) })] })) : asImage ? (_jsx(FadeInImage, { src: item.url, alt: item.name, loading: "lazy", className: "absolute inset-0 size-full object-cover" })) : (_jsx("span", { className: "flex size-full items-center justify-center", children: _jsx(MediaKindIcon, { kind: kindOf(item.kind), className: "size-8 text-white/90 drop-shadow-sm" }) })), multiple ? (_jsx("span", { className: "absolute left-1.5 top-1.5 grid size-5 place-items-center rounded bg-black/60 text-[11px] font-medium tabular-nums text-white", title: intl.formatMessage(messages.position, {
102
103
  index: index + 1,
103
104
  total
104
105
  }), children: index + 1 })) : null, item.pending ? (_jsx("span", { className: "absolute bottom-1.5 left-1.5 rounded bg-warning-soft px-1.5 py-0.5 text-[10px] font-medium leading-none text-warning-soft-foreground shadow-sm", children: intl.formatMessage(messages.pending) })) : null, _jsxs("div", { className: "absolute right-1.5 top-1.5 flex items-center gap-0.5 rounded-md bg-background/85 p-0.5 shadow-sm backdrop-blur transition-opacity focus-within:opacity-100 group-hover/item:opacity-100 sm:opacity-0", children: [item.missing || item.pending || item.resolving ? null : (_jsx(Button, { asChild: true, variant: "ghost", size: "icon", className: "size-6", children: _jsx("a", { href: item.originalUrl ?? item.url, target: "_blank", rel: "noreferrer", "aria-label": intl.formatMessage(messages.preview, { name: item.name }), children: _jsx(ExternalLink, { className: "size-3.5", "aria-hidden": true }) }) })), readOnly ? null : (_jsx(Button, { type: "button", variant: "ghost", size: "icon", className: "size-6 text-muted-foreground hover:bg-destructive/10 hover:text-destructive", "aria-label": intl.formatMessage(messages.remove, {
@@ -6,9 +6,15 @@ import { type AssetActionKind } from '../../AssetActionsMenu';
6
6
  * size/dimensions line. Clicking the thumbnail or name opens the detail drawer;
7
7
  * the checkbox toggles selection. A selected tile gets a primary ring.
8
8
  */
9
- export declare function MediaAssetCard({ asset, selected, onToggleSelect, onAction, canCreate, canUpdate, canDelete }: {
9
+ export declare function MediaAssetCard({ asset, selected, onToggleSelect, onAction, canCreate, canUpdate, canDelete, justUploaded }: {
10
10
  asset: MediaAsset;
11
11
  selected: boolean;
12
+ /**
13
+ * The asset was just uploaded: the tile rises into its slot. Not
14
+ * `ds-flash` — its keyframes animate `background-color` to transparent,
15
+ * which would drop the tile's card background for the length of the fade.
16
+ */
17
+ justUploaded?: boolean;
12
18
  onToggleSelect: (id: string) => void;
13
19
  onAction: (kind: AssetActionKind, asset: MediaAsset) => void;
14
20
  canCreate: boolean;
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../../src/lib/components/MediaGrid/MediaAssetCard/index.tsx"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,2BAA2B,CAAC;AAG5D,OAAO,EAAoB,KAAK,eAAe,EAAE,MAAM,wBAAwB,CAAC;AAShF;;;;;GAKG;AACH,wBAAgB,cAAc,CAAC,EAC3B,KAAK,EACL,QAAQ,EACR,cAAc,EACd,QAAQ,EACR,SAAS,EACT,SAAS,EACT,SAAS,EACZ,EAAE;IACC,KAAK,EAAE,UAAU,CAAC;IAClB,QAAQ,EAAE,OAAO,CAAC;IAClB,cAAc,EAAE,CAAC,EAAE,EAAE,MAAM,KAAK,IAAI,CAAC;IACrC,QAAQ,EAAE,CAAC,IAAI,EAAE,eAAe,EAAE,KAAK,EAAE,UAAU,KAAK,IAAI,CAAC;IAC7D,SAAS,EAAE,OAAO,CAAC;IACnB,SAAS,EAAE,OAAO,CAAC;IACnB,SAAS,EAAE,OAAO,CAAC;CACtB,+BAsEA"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../../src/lib/components/MediaGrid/MediaAssetCard/index.tsx"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,2BAA2B,CAAC;AAG5D,OAAO,EAAoB,KAAK,eAAe,EAAE,MAAM,wBAAwB,CAAC;AAShF;;;;;GAKG;AACH,wBAAgB,cAAc,CAAC,EAC3B,KAAK,EACL,QAAQ,EACR,cAAc,EACd,QAAQ,EACR,SAAS,EACT,SAAS,EACT,SAAS,EACT,YAAoB,EACvB,EAAE;IACC,KAAK,EAAE,UAAU,CAAC;IAClB,QAAQ,EAAE,OAAO,CAAC;IAClB;;;;OAIG;IACH,YAAY,CAAC,EAAE,OAAO,CAAC;IACvB,cAAc,EAAE,CAAC,EAAE,EAAE,MAAM,KAAK,IAAI,CAAC;IACrC,QAAQ,EAAE,CAAC,IAAI,EAAE,eAAe,EAAE,KAAK,EAAE,UAAU,KAAK,IAAI,CAAC;IAC7D,SAAS,EAAE,OAAO,CAAC;IACnB,SAAS,EAAE,OAAO,CAAC;IACnB,SAAS,EAAE,OAAO,CAAC;CACtB,+BAuEA"}
@@ -16,10 +16,10 @@ const messages = defineMessages({
16
16
  * size/dimensions line. Clicking the thumbnail or name opens the detail drawer;
17
17
  * the checkbox toggles selection. A selected tile gets a primary ring.
18
18
  */
19
- export function MediaAssetCard({ asset, selected, onToggleSelect, onAction, canCreate, canUpdate, canDelete }) {
19
+ export function MediaAssetCard({ asset, selected, onToggleSelect, onAction, canCreate, canUpdate, canDelete, justUploaded = false }) {
20
20
  const intl = useIntl();
21
21
  const handlers = buildAssetHandlers(asset, onAction);
22
- return (_jsxs("div", { className: cn('group relative overflow-hidden rounded-xl border bg-card transition-shadow hover:shadow-md', selected && 'ring-2 ring-primary ring-offset-1'), children: [_jsxs("div", { className: "relative", children: [_jsx("button", { type: "button", onClick: () => onAction('open', asset), "aria-label": intl.formatMessage(messages.open, {
22
+ return (_jsxs("div", { className: cn('group relative overflow-hidden rounded-xl border bg-card transition-shadow hover:shadow-md', selected && 'ring-2 ring-primary ring-offset-1', justUploaded && 'ds-item-enter'), children: [_jsxs("div", { className: "relative", children: [_jsx("button", { type: "button", onClick: () => onAction('open', asset), "aria-label": intl.formatMessage(messages.open, {
23
23
  name: asset.name
24
24
  }), className: "block w-full focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring", children: _jsx(MediaThumbnail, { asset: asset, className: "aspect-[4/3] w-full" }) }), _jsx("div", { className: cn('absolute left-2 top-2 rounded-md bg-background/80 p-0.5 shadow-sm backdrop-blur transition-opacity focus-within:opacity-100 group-hover:opacity-100', selected ? 'opacity-100' : 'opacity-0'), children: _jsx(Checkbox, { checked: selected, onCheckedChange: () => onToggleSelect(asset.id), "aria-label": intl.formatMessage(messages.select, {
25
25
  name: asset.name
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../src/lib/components/MediaGrid/index.tsx"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,wBAAwB,CAAC;AACzD,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,yBAAyB,CAAC;AAC3D,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,qBAAqB,CAAC;AAU3D;;;;;GAKG;AACH,wBAAgB,SAAS,CAAC,EACtB,OAAO,EACP,MAAM,EACN,YAAY,EACZ,WAAW,EACX,YAAY,EACZ,cAAc,EACd,cAAc,EACd,cAAc,EACd,aAAa,EACb,SAAS,EACT,SAAS,EACT,SAAS,EACZ,EAAE;IACC,OAAO,EAAE,WAAW,EAAE,CAAC;IACvB,MAAM,EAAE,UAAU,EAAE,CAAC;IACrB,YAAY,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAClC,WAAW,EAAE,GAAG,CAAC,MAAM,CAAC,CAAC;IACzB,YAAY,EAAE,CAAC,QAAQ,EAAE,MAAM,KAAK,IAAI,CAAC;IACzC,cAAc,EAAE,CAAC,MAAM,EAAE,WAAW,KAAK,IAAI,CAAC;IAC9C,cAAc,EAAE,CAAC,MAAM,EAAE,WAAW,KAAK,IAAI,CAAC;IAC9C,cAAc,EAAE,CAAC,EAAE,EAAE,MAAM,KAAK,IAAI,CAAC;IACrC,aAAa,EAAE,CAAC,IAAI,EAAE,eAAe,EAAE,KAAK,EAAE,UAAU,KAAK,IAAI,CAAC;IAClE,SAAS,EAAE,OAAO,CAAC;IACnB,SAAS,EAAE,OAAO,CAAC;IACnB,SAAS,EAAE,OAAO,CAAC;CACtB,+BAuDA"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../src/lib/components/MediaGrid/index.tsx"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,wBAAwB,CAAC;AACzD,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,yBAAyB,CAAC;AAC3D,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,qBAAqB,CAAC;AAU3D;;;;;GAKG;AACH,wBAAgB,SAAS,CAAC,EACtB,OAAO,EACP,MAAM,EACN,YAAY,EACZ,WAAW,EACX,YAAY,EACZ,cAAc,EACd,cAAc,EACd,cAAc,EACd,aAAa,EACb,SAAS,EACT,SAAS,EACT,SAAS,EACZ,EAAE;IACC,OAAO,EAAE,WAAW,EAAE,CAAC;IACvB,MAAM,EAAE,UAAU,EAAE,CAAC;IACrB,YAAY,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAClC,WAAW,EAAE,GAAG,CAAC,MAAM,CAAC,CAAC;IACzB,YAAY,EAAE,CAAC,QAAQ,EAAE,MAAM,KAAK,IAAI,CAAC;IACzC,cAAc,EAAE,CAAC,MAAM,EAAE,WAAW,KAAK,IAAI,CAAC;IAC9C,cAAc,EAAE,CAAC,MAAM,EAAE,WAAW,KAAK,IAAI,CAAC;IAC9C,cAAc,EAAE,CAAC,EAAE,EAAE,MAAM,KAAK,IAAI,CAAC;IACrC,aAAa,EAAE,CAAC,IAAI,EAAE,eAAe,EAAE,KAAK,EAAE,UAAU,KAAK,IAAI,CAAC;IAClE,SAAS,EAAE,OAAO,CAAC;IACnB,SAAS,EAAE,OAAO,CAAC;IACnB,SAAS,EAAE,OAAO,CAAC;CACtB,+BAyDA"}
@@ -1,5 +1,7 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { defineMessages, useIntl } from 'react-intl';
3
+ import { useJustCreated } from '@orthacms/utils-admin';
4
+ import { MEDIA_ASSETS_SCOPE } from '../../hooks/useUploadQueue';
3
5
  import { MediaFolderCard } from './MediaFolderCard';
4
6
  import { MediaAssetCard } from './MediaAssetCard';
5
7
  /** Intl descriptors for {@link MediaGrid} section labels, co-located. */
@@ -15,6 +17,7 @@ const messages = defineMessages({
15
17
  */
16
18
  export function MediaGrid({ folders, assets, folderCounts, selectedIds, onOpenFolder, onRenameFolder, onDeleteFolder, onToggleSelect, onAssetAction, canCreate, canUpdate, canDelete }) {
17
19
  const intl = useIntl();
20
+ const isJustUploaded = useJustCreated(MEDIA_ASSETS_SCOPE);
18
21
  const showLabels = folders.length > 0 && assets.length > 0;
19
- return (_jsxs("div", { className: "space-y-6", children: [folders.length > 0 ? (_jsxs("section", { children: [showLabels ? (_jsx("h2", { className: "mb-2 text-xs font-semibold uppercase tracking-wide text-muted-foreground", children: intl.formatMessage(messages.folders) })) : null, _jsx("div", { className: "grid grid-cols-1 gap-3 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4", children: folders.map((folder) => (_jsx(MediaFolderCard, { folder: folder, assetCount: folderCounts.get(folder.id) ?? 0, onOpen: onOpenFolder, onRename: onRenameFolder, onDelete: onDeleteFolder, canUpdate: canUpdate, canDelete: canDelete }, folder.id))) })] })) : null, assets.length > 0 ? (_jsxs("section", { children: [showLabels ? (_jsx("h2", { className: "mb-2 text-xs font-semibold uppercase tracking-wide text-muted-foreground", children: intl.formatMessage(messages.files) })) : null, _jsx("div", { className: "grid grid-cols-2 gap-3 sm:grid-cols-3 lg:grid-cols-4 xl:grid-cols-5", children: assets.map((asset) => (_jsx(MediaAssetCard, { asset: asset, selected: selectedIds.has(asset.id), onToggleSelect: onToggleSelect, onAction: onAssetAction, canCreate: canCreate, canUpdate: canUpdate, canDelete: canDelete }, asset.id))) })] })) : null] }));
22
+ return (_jsxs("div", { className: "space-y-6", children: [folders.length > 0 ? (_jsxs("section", { children: [showLabels ? (_jsx("h2", { className: "mb-2 text-xs font-semibold uppercase tracking-wide text-muted-foreground", children: intl.formatMessage(messages.folders) })) : null, _jsx("div", { className: "grid grid-cols-1 gap-3 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4", children: folders.map((folder) => (_jsx(MediaFolderCard, { folder: folder, assetCount: folderCounts.get(folder.id) ?? 0, onOpen: onOpenFolder, onRename: onRenameFolder, onDelete: onDeleteFolder, canUpdate: canUpdate, canDelete: canDelete }, folder.id))) })] })) : null, assets.length > 0 ? (_jsxs("section", { children: [showLabels ? (_jsx("h2", { className: "mb-2 text-xs font-semibold uppercase tracking-wide text-muted-foreground", children: intl.formatMessage(messages.files) })) : null, _jsx("div", { className: "grid grid-cols-2 gap-3 sm:grid-cols-3 lg:grid-cols-4 xl:grid-cols-5", children: assets.map((asset) => (_jsx(MediaAssetCard, { asset: asset, selected: selectedIds.has(asset.id), justUploaded: isJustUploaded(asset.id), onToggleSelect: onToggleSelect, onAction: onAssetAction, canCreate: canCreate, canUpdate: canUpdate, canDelete: canDelete }, asset.id))) })] })) : null] }));
20
23
  }
@@ -4,8 +4,9 @@ import type { MediaAsset } from '../../types/mediaAsset';
4
4
  * from the media API — the small `thumb` derivative by default, or the larger
5
5
  * `preview` when `size="preview"` (the detail drawer) — falling back to the
6
6
  * original when no derivative exists (e.g. SVG). A deterministic gradient tile
7
- * sits underneath as the backdrop / loading state; non-image kinds show it with
8
- * a centred kind glyph. Video/audio get a play affordance and a duration pill.
7
+ * sits underneath as the backdrop / loading state — the image fades in over it
8
+ * once loaded (`FadeInImage`) — and non-image kinds show it with a centred kind
9
+ * glyph. Video/audio get a play affordance and a duration pill.
9
10
  * Sized entirely by the parent via `className` (aspect + radius), so it serves
10
11
  * tiles, list rows, and the drawer.
11
12
  */
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../src/lib/components/MediaThumbnail/index.tsx"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,wBAAwB,CAAC;AAKzD;;;;;;;;;GASG;AACH,wBAAgB,cAAc,CAAC,EAC3B,KAAK,EACL,SAAS,EACT,aAAa,EACb,IAAc,EACjB,EAAE;IACC,KAAK,EAAE,UAAU,CAAC;IAClB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,gFAAgF;IAChF,IAAI,CAAC,EAAE,OAAO,GAAG,SAAS,CAAC;CAC9B,+BA4CA"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../src/lib/components/MediaThumbnail/index.tsx"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,wBAAwB,CAAC;AAMzD;;;;;;;;;;GAUG;AACH,wBAAgB,cAAc,CAAC,EAC3B,KAAK,EACL,SAAS,EACT,aAAa,EACb,IAAc,EACjB,EAAE;IACC,KAAK,EAAE,UAAU,CAAC;IAClB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,gFAAgF;IAChF,IAAI,CAAC,EAAE,OAAO,GAAG,SAAS,CAAC;CAC9B,+BA4CA"}
@@ -4,14 +4,16 @@ import { cn } from '@orthacms/design-system';
4
4
  import { MEDIA_KIND } from '../../constants';
5
5
  import { assetGradient } from '../../utils/assetGradient';
6
6
  import { formatDuration } from '../../utils/formatDuration';
7
+ import { FadeInImage } from '../FadeInImage';
7
8
  import { MediaKindIcon } from '../MediaKindIcon';
8
9
  /**
9
10
  * The visual preview for an asset. Image assets render a real thumbnail served
10
11
  * from the media API — the small `thumb` derivative by default, or the larger
11
12
  * `preview` when `size="preview"` (the detail drawer) — falling back to the
12
13
  * original when no derivative exists (e.g. SVG). A deterministic gradient tile
13
- * sits underneath as the backdrop / loading state; non-image kinds show it with
14
- * a centred kind glyph. Video/audio get a play affordance and a duration pill.
14
+ * sits underneath as the backdrop / loading state — the image fades in over it
15
+ * once loaded (`FadeInImage`) — and non-image kinds show it with a centred kind
16
+ * glyph. Video/audio get a play affordance and a duration pill.
15
17
  * Sized entirely by the parent via `className` (aspect + radius), so it serves
16
18
  * tiles, list rows, and the drawer.
17
19
  */
@@ -22,5 +24,5 @@ export function MediaThumbnail({ asset, className, iconClassName, size = 'thumb'
22
24
  const imageSrc = size === 'preview'
23
25
  ? (asset.previewUrl ?? asset.thumbUrl ?? asset.url)
24
26
  : (asset.thumbUrl ?? asset.previewUrl ?? asset.url);
25
- return (_jsxs("div", { className: cn('relative flex items-center justify-center overflow-hidden bg-muted', className), style: { backgroundImage: assetGradient(asset.id) }, children: [_jsx("div", { className: "absolute inset-0 bg-black/5", "aria-hidden": true }), isImage ? (_jsx("img", { src: imageSrc, alt: asset.alt ?? '', loading: "lazy", className: "absolute inset-0 size-full object-cover" })) : (_jsx(MediaKindIcon, { kind: asset.kind, className: cn('relative size-8 text-white/90 drop-shadow-sm', iconClassName) })), isPlayable ? (_jsxs("span", { className: "absolute bottom-1.5 right-1.5 inline-flex items-center gap-1 rounded-md bg-black/55 px-1.5 py-0.5 text-[11px] font-medium text-white", children: [_jsx(Play, { className: "size-3 fill-current", "aria-hidden": true }), asset.duration ? formatDuration(asset.duration) : null] })) : null] }));
27
+ return (_jsxs("div", { className: cn('relative flex items-center justify-center overflow-hidden bg-muted', className), style: { backgroundImage: assetGradient(asset.id) }, children: [_jsx("div", { className: "absolute inset-0 bg-black/5", "aria-hidden": true }), isImage ? (_jsx(FadeInImage, { src: imageSrc, alt: asset.alt ?? '', loading: "lazy", className: "absolute inset-0 size-full object-cover" })) : (_jsx(MediaKindIcon, { kind: asset.kind, className: cn('relative size-8 text-white/90 drop-shadow-sm', iconClassName) })), isPlayable ? (_jsxs("span", { className: "absolute bottom-1.5 right-1.5 inline-flex items-center gap-1 rounded-md bg-black/55 px-1.5 py-0.5 text-[11px] font-medium text-white", children: [_jsx(Play, { className: "size-3 fill-current", "aria-hidden": true }), asset.duration ? formatDuration(asset.duration) : null] })) : null] }));
26
28
  }
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../../src/lib/components/UploadDialog/StagedFileRow/index.tsx"],"names":[],"mappings":"AAiDA;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,aAAa,CAAC,EAC1B,IAAI,EACJ,GAAQ,EACR,UAAkB,EAClB,WAAW,EACX,QAAQ,EACX,EAAE;IACC,IAAI,EAAE,IAAI,CAAC;IACX,6CAA6C;IAC7C,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,yEAAyE;IACzE,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,WAAW,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,IAAI,CAAC;IACpC,QAAQ,EAAE,MAAM,IAAI,CAAC;CACxB,+BAyEA"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../../src/lib/components/UploadDialog/StagedFileRow/index.tsx"],"names":[],"mappings":"AAkDA;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,aAAa,CAAC,EAC1B,IAAI,EACJ,GAAQ,EACR,UAAkB,EAClB,WAAW,EACX,QAAQ,EACX,EAAE;IACC,IAAI,EAAE,IAAI,CAAC;IACX,6CAA6C;IAC7C,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,yEAAyE;IACzE,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,WAAW,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,IAAI,CAAC;IACpC,QAAQ,EAAE,MAAM,IAAI,CAAC;CACxB,+BAyEA"}
@@ -4,6 +4,7 @@ import { defineMessages, useIntl } from 'react-intl';
4
4
  import { Button, InputField } from '@orthacms/design-system';
5
5
  import { FileText, Film, Music, Package, X } from 'lucide-react';
6
6
  import { formatBytes } from '../../../utils/formatBytes';
7
+ import { FadeInImage } from '../../FadeInImage';
7
8
  /** Intl descriptors for {@link StagedFileRow}, co-located. */
8
9
  const messages = defineMessages({
9
10
  remove: { id: 'media.upload.removeFile', defaultMessage: 'Remove {name}' },
@@ -74,7 +75,7 @@ export function StagedFileRow({ file, alt = '', collectAlt = false, onAltChange,
74
75
  return () => URL.revokeObjectURL(url);
75
76
  }, [file, isImage]);
76
77
  const Glyph = glyphFor(file.type);
77
- return (_jsxs("li", { className: "flex flex-col gap-2 rounded-lg border bg-background px-3 py-2 text-sm", children: [_jsxs("div", { className: "flex items-center gap-3", children: [_jsx("span", { className: "flex size-10 shrink-0 items-center justify-center overflow-hidden rounded-md bg-muted", children: previewUrl ? (_jsx("img", { src: previewUrl, alt: "", className: "size-full object-cover" })) : (_jsx(Glyph, { className: "size-4 text-muted-foreground", "aria-hidden": true })) }), _jsx("span", { className: "min-w-0 flex-1 truncate", children: file.name }), _jsx("span", { className: "shrink-0 text-xs tabular-nums text-muted-foreground", children: formatBytes(file.size) }), _jsx(Button, { variant: "ghost", size: "icon", className: "size-7 shrink-0 shadow-none", "aria-label": intl.formatMessage(messages.remove, {
78
+ return (_jsxs("li", { className: "flex flex-col gap-2 rounded-lg border bg-background px-3 py-2 text-sm", children: [_jsxs("div", { className: "flex items-center gap-3", children: [_jsx("span", { className: "flex size-10 shrink-0 items-center justify-center overflow-hidden rounded-md bg-muted", children: previewUrl ? (_jsx(FadeInImage, { src: previewUrl, alt: "", className: "size-full object-cover" })) : (_jsx(Glyph, { className: "size-4 text-muted-foreground", "aria-hidden": true })) }), _jsx("span", { className: "min-w-0 flex-1 truncate", children: file.name }), _jsx("span", { className: "shrink-0 text-xs tabular-nums text-muted-foreground", children: formatBytes(file.size) }), _jsx(Button, { variant: "ghost", size: "icon", className: "size-7 shrink-0 shadow-none", "aria-label": intl.formatMessage(messages.remove, {
78
79
  name: file.name
79
80
  }), onClick: onRemove, children: _jsx(X, { "aria-hidden": true }) })] }), collectAlt && isImage ? (_jsx(InputField, { id: `staged-alt-${altId}`, label: intl.formatMessage(messages.altLabel), "aria-label": intl.formatMessage(messages.altLabelFor, {
80
81
  name: file.name
@@ -79,4 +79,17 @@ export declare const MEDIA_SORT: {
79
79
  };
80
80
  /** One asset-ordering option. */
81
81
  export type MediaSort = (typeof MEDIA_SORT)[keyof typeof MEDIA_SORT];
82
+ /**
83
+ * The folders column's entry in the shell's panel layout, and its bounds in
84
+ * pixels. Resizable on a desktop only — below `lg` the folders open in a
85
+ * drawer, which has no edge to drag.
86
+ */
87
+ export declare const MEDIA_FOLDERS_PANEL: {
88
+ readonly id: "media.folders";
89
+ readonly defaultWidth: 240;
90
+ readonly minWidth: 192;
91
+ readonly maxWidth: 384;
92
+ readonly maxViewportRatio: 0.2;
93
+ readonly cssVar: "--media-folders-width";
94
+ };
82
95
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/lib/constants/index.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,+EAA+E;AAC/E,eAAO,MAAM,UAAU,eAAe,CAAC;AAEvC,qDAAqD;AACrD,eAAO,MAAM,YAAY,iBAAiB,CAAC;AAE3C,6DAA6D;AAC7D,eAAO,MAAM,YAAY,iBAAiB,CAAC;AAE3C,wDAAwD;AACxD,eAAO,MAAM,YAAY,iBAAiB,CAAC;AAE3C;;;;GAIG;AACH,eAAO,MAAM,aAAa,UAAU,CAAC;AAErC,4EAA4E;AAC5E,eAAO,MAAM,cAAc,SAAS,CAAC;AAErC,0CAA0C;AAC1C,eAAO,MAAM,aAAa;IACtB,8CAA8C;;IAE9C,6BAA6B;;;;IAI7B,8CAA8C;;CAExC,CAAC;AAEX,iCAAiC;AACjC,MAAM,MAAM,YAAY,GAAG,CAAC,OAAO,aAAa,CAAC,CAAC,MAAM,OAAO,aAAa,CAAC,CAAC;AAE9E;;;;GAIG;AACH,eAAO,MAAM,kBAAkB,IAAI,CAAC;AAEpC,4EAA4E;AAC5E,eAAO,MAAM,UAAU;;;;;;CAMb,CAAC;AAEX,iCAAiC;AACjC,MAAM,MAAM,SAAS,GAAG,CAAC,OAAO,UAAU,CAAC,CAAC,MAAM,OAAO,UAAU,CAAC,CAAC;AAErE,oEAAoE;AACpE,eAAO,MAAM,eAAe,QAAQ,CAAC;AAErC;;;;;;GAMG;AACH,eAAO,MAAM,wBAAwB,KAAK,CAAC;AAE3C,iEAAiE;AACjE,eAAO,MAAM,wBAAwB,UAAe,CAAC;AAErD;;;;GAIG;AACH,eAAO,MAAM,uBAAuB,MAAM,CAAC;AAE3C,yEAAyE;AACzE,eAAO,MAAM,kBAAkB,MAAM,CAAC;AAEtC,0CAA0C;AAC1C,eAAO,MAAM,UAAU;;;;;;;CAOb,CAAC;AAEX,iCAAiC;AACjC,MAAM,MAAM,SAAS,GAAG,CAAC,OAAO,UAAU,CAAC,CAAC,MAAM,OAAO,UAAU,CAAC,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/lib/constants/index.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,+EAA+E;AAC/E,eAAO,MAAM,UAAU,eAAe,CAAC;AAEvC,qDAAqD;AACrD,eAAO,MAAM,YAAY,iBAAiB,CAAC;AAE3C,6DAA6D;AAC7D,eAAO,MAAM,YAAY,iBAAiB,CAAC;AAE3C,wDAAwD;AACxD,eAAO,MAAM,YAAY,iBAAiB,CAAC;AAE3C;;;;GAIG;AACH,eAAO,MAAM,aAAa,UAAU,CAAC;AAErC,4EAA4E;AAC5E,eAAO,MAAM,cAAc,SAAS,CAAC;AAErC,0CAA0C;AAC1C,eAAO,MAAM,aAAa;IACtB,8CAA8C;;IAE9C,6BAA6B;;;;IAI7B,8CAA8C;;CAExC,CAAC;AAEX,iCAAiC;AACjC,MAAM,MAAM,YAAY,GAAG,CAAC,OAAO,aAAa,CAAC,CAAC,MAAM,OAAO,aAAa,CAAC,CAAC;AAE9E;;;;GAIG;AACH,eAAO,MAAM,kBAAkB,IAAI,CAAC;AAEpC,4EAA4E;AAC5E,eAAO,MAAM,UAAU;;;;;;CAMb,CAAC;AAEX,iCAAiC;AACjC,MAAM,MAAM,SAAS,GAAG,CAAC,OAAO,UAAU,CAAC,CAAC,MAAM,OAAO,UAAU,CAAC,CAAC;AAErE,oEAAoE;AACpE,eAAO,MAAM,eAAe,QAAQ,CAAC;AAErC;;;;;;GAMG;AACH,eAAO,MAAM,wBAAwB,KAAK,CAAC;AAE3C,iEAAiE;AACjE,eAAO,MAAM,wBAAwB,UAAe,CAAC;AAErD;;;;GAIG;AACH,eAAO,MAAM,uBAAuB,MAAM,CAAC;AAE3C,yEAAyE;AACzE,eAAO,MAAM,kBAAkB,MAAM,CAAC;AAEtC,0CAA0C;AAC1C,eAAO,MAAM,UAAU;;;;;;;CAOb,CAAC;AAEX,iCAAiC;AACjC,MAAM,MAAM,SAAS,GAAG,CAAC,OAAO,UAAU,CAAC,CAAC,MAAM,OAAO,UAAU,CAAC,CAAC;AAErE;;;;GAIG;AACH,eAAO,MAAM,mBAAmB;;;;;;;CAOtB,CAAC"}
@@ -73,3 +73,16 @@ export const MEDIA_SORT = {
73
73
  Largest: 'largest',
74
74
  Smallest: 'smallest'
75
75
  };
76
+ /**
77
+ * The folders column's entry in the shell's panel layout, and its bounds in
78
+ * pixels. Resizable on a desktop only — below `lg` the folders open in a
79
+ * drawer, which has no edge to drag.
80
+ */
81
+ export const MEDIA_FOLDERS_PANEL = {
82
+ id: 'media.folders',
83
+ defaultWidth: 240,
84
+ minWidth: 192,
85
+ maxWidth: 384,
86
+ maxViewportRatio: 0.2,
87
+ cssVar: '--media-folders-width'
88
+ };
@@ -1,5 +1,7 @@
1
1
  import type { UploadItem, UploadSummary } from '../../types/upload';
2
2
  import type { StagedUpload } from '../../infrastructure/mediaGateway';
3
+ /** The `justCreated` scope of the library's asset tiles. */
4
+ export declare const MEDIA_ASSETS_SCOPE = "media-assets";
3
5
  /**
4
6
  * The Media Library's upload queue: turns a set of picked files into a list of
5
7
  * progress-tracked items, uploading at most {@link UPLOAD_CONCURRENCY} at once.
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../src/lib/hooks/useUploadQueue/index.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,UAAU,EAAE,aAAa,EAAE,MAAM,oBAAoB,CAAC;AAGpE,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,mCAAmC,CAAC;AAiBtE;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,wBAAgB,cAAc,CAAC,QAAQ,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,IAAI;;;uBAiHrD,YAAY,EAAE;gBAsBnB,MAAM;iBAeN,MAAM;;EAuElB"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../src/lib/hooks/useUploadQueue/index.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,UAAU,EAAE,aAAa,EAAE,MAAM,oBAAoB,CAAC;AAGpE,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,mCAAmC,CAAC;AAiBtE,4DAA4D;AAC5D,eAAO,MAAM,kBAAkB,iBAAiB,CAAC;AAEjD;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,wBAAgB,cAAc,CAAC,QAAQ,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,IAAI;;;uBA4HrD,YAAY,EAAE;gBAsBnB,MAAM;iBAeN,MAAM;;EAuElB"}
@@ -1,5 +1,6 @@
1
1
  import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
2
2
  import { defineMessages, useIntl } from 'react-intl';
3
+ import { markJustCreated } from '@orthacms/utils-admin';
3
4
  import { UPLOAD_CONCURRENCY, UPLOAD_STATUS } from '../../constants';
4
5
  import { httpMediaGateway } from '../../infrastructure/httpMediaGateway';
5
6
  import { apiMessage } from '../../infrastructure/apiMessage';
@@ -16,6 +17,8 @@ const SETTLED = [
16
17
  UPLOAD_STATUS.Failed,
17
18
  UPLOAD_STATUS.Cancelled
18
19
  ];
20
+ /** The `justCreated` scope of the library's asset tiles. */
21
+ export const MEDIA_ASSETS_SCOPE = 'media-assets';
19
22
  /**
20
23
  * The Media Library's upload queue: turns a set of picked files into a list of
21
24
  * progress-tracked items, uploading at most {@link UPLOAD_CONCURRENCY} at once.
@@ -51,6 +54,8 @@ export function useUploadQueue(folderId, onUploaded) {
51
54
  const activeRef = useRef(0);
52
55
  /** Whether the current batch has produced a success worth invalidating for. */
53
56
  const successRef = useRef(false);
57
+ /** Ids of the assets this batch created, marked when the batch settles. */
58
+ const createdRef = useRef([]);
54
59
  const nextIdRef = useRef(0);
55
60
  // `pump` and `runOne` call each other; the ref breaks the cycle without
56
61
  // rebuilding either callback on every render.
@@ -73,6 +78,14 @@ export function useUploadQueue(folderId, onUploaded) {
73
78
  if (activeRef.current === 0 && waitingRef.current.length === 0) {
74
79
  if (successRef.current) {
75
80
  successRef.current = false;
81
+ // Marked here rather than per file: the mark lasts a few
82
+ // seconds, and the grid only learns of the files from the one
83
+ // refetch this triggers — a long batch would outlive marks
84
+ // set as each file landed.
85
+ for (const assetId of createdRef.current) {
86
+ markJustCreated(MEDIA_ASSETS_SCOPE, assetId);
87
+ }
88
+ createdRef.current = [];
76
89
  onUploadedRef.current();
77
90
  }
78
91
  }
@@ -93,8 +106,9 @@ export function useUploadQueue(folderId, onUploaded) {
93
106
  signal: controller.signal,
94
107
  onProgress: (percent) => patch(id, { progress: percent })
95
108
  })
96
- .then(() => {
109
+ .then((asset) => {
97
110
  successRef.current = true;
111
+ createdRef.current.push(asset.id);
98
112
  patch(id, {
99
113
  status: UPLOAD_STATUS.Done,
100
114
  progress: 100,
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../src/lib/pages/MediaLibraryPage/index.tsx"],"names":[],"mappings":"AA2KA;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,wBAAgB,gBAAgB,gCAqkB/B"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../src/lib/pages/MediaLibraryPage/index.tsx"],"names":[],"mappings":"AA+KA;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,wBAAgB,gBAAgB,gCAwlB/B"}
@@ -1,10 +1,10 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { useRef, useState } from 'react';
3
3
  import { defineMessages, useIntl } from 'react-intl';
4
- import { Button, ConfirmDialog, Drawer, DrawerContent, DrawerTitle, cn, toast } from '@orthacms/design-system';
4
+ import { Button, ConfirmDialog, Drawer, DrawerContent, DrawerTitle, ResizeHandle, cn, toast, useResizablePanel } from '@orthacms/design-system';
5
5
  import { PanelLeft } from 'lucide-react';
6
6
  import { useHasPermission } from '@orthacms/identity-admin';
7
- import { MEDIA_CREATE, MEDIA_DELETE, MEDIA_READ, MEDIA_UPDATE, ROOT_FOLDER_ID } from '../../constants';
7
+ import { MEDIA_CREATE, MEDIA_DELETE, MEDIA_FOLDERS_PANEL, MEDIA_READ, MEDIA_UPDATE, ROOT_FOLDER_ID } from '../../constants';
8
8
  import { useMediaLibrary } from '../../hooks/useMediaLibrary';
9
9
  import { MediaFoldersNav } from '../../components/MediaFoldersNav';
10
10
  import { MediaTopBar } from '../../components/MediaTopBar';
@@ -39,6 +39,7 @@ const messages = defineMessages({
39
39
  },
40
40
  retry: { id: 'media.page.retry', defaultMessage: 'Try again' },
41
41
  openNav: { id: 'media.page.openNav', defaultMessage: 'Folders' },
42
+ resizeNav: { id: 'media.page.resizeNav', defaultMessage: 'Resize folders' },
42
43
  navTitle: {
43
44
  id: 'media.page.navTitle',
44
45
  defaultMessage: 'Folder navigation'
@@ -170,6 +171,7 @@ export function MediaLibraryPage() {
170
171
  /** Set by a confirmed delete, read once by the dialog's close-focus hook. */
171
172
  const focusGridOnCloseRef = useRef(false);
172
173
  const [navOpen, setNavOpen] = useState(false);
174
+ const foldersPanel = useResizablePanel(MEDIA_FOLDERS_PANEL);
173
175
  const [newFolderOpen, setNewFolderOpen] = useState(false);
174
176
  const [uploadOpen, setUploadOpen] = useState(false);
175
177
  const [renameTarget, setRenameTarget] = useState(null);
@@ -346,7 +348,7 @@ export function MediaLibraryPage() {
346
348
  if (store.isError) {
347
349
  return (_jsxs("div", { className: "flex min-h-0 flex-1 flex-col items-center justify-center gap-3 p-6 text-center", children: [_jsx("p", { className: "max-w-sm text-sm text-muted-foreground", role: "alert", children: intl.formatMessage(messages.loadError) }), _jsx(Button, { variant: "outline", onClick: store.reload, children: intl.formatMessage(messages.retry) })] }));
348
350
  }
349
- return (_jsxs("div", { className: "flex min-h-0 min-w-0 flex-1 flex-col", children: [_jsx(MediaTopBar, { breadcrumbs: store.breadcrumbs, onNavigate: store.navigateTo }), _jsxs("div", { className: "flex min-h-0 min-w-0 flex-1 flex-col lg:flex-row", children: [_jsx("div", { className: "border-b p-3 lg:hidden", children: _jsxs(Button, { variant: "outline", size: "sm", className: "shadow-none", onClick: () => setNavOpen(true), children: [_jsx(PanelLeft, { "aria-hidden": true }), intl.formatMessage(messages.openNav)] }) }), _jsx(MediaFoldersNav, { className: "hidden lg:flex lg:border-r", folders: store.folders, currentFolderId: store.currentFolderId, counts: store.folderCounts, rootCount: store.folderCounts.get(ROOT_FOLDER_ID) ?? 0, onNavigate: store.navigateTo, onNewFolder: () => setNewFolderOpen(true), canCreate: canCreate }), _jsx("div", { className: "min-w-0 flex-1 overflow-auto", children: _jsxs("div", { className: "p-4 sm:p-6", children: [_jsxs("div", { className: "mb-4", children: [_jsx("h1", { className: "text-2xl font-semibold tracking-[-0.01em]", children: store.currentFolder
351
+ return (_jsxs("div", { className: "flex min-h-0 min-w-0 flex-1 flex-col", children: [_jsx(MediaTopBar, { breadcrumbs: store.breadcrumbs, onNavigate: store.navigateTo }), _jsxs("div", { className: "flex min-h-0 min-w-0 flex-1 flex-col lg:flex-row", children: [_jsx("div", { className: "border-b p-3 lg:hidden", children: _jsxs(Button, { variant: "outline", size: "sm", className: "shadow-none", onClick: () => setNavOpen(true), children: [_jsx(PanelLeft, { "aria-hidden": true }), intl.formatMessage(messages.openNav)] }) }), _jsxs("div", { ref: foldersPanel.ref, style: foldersPanel.style, className: "relative hidden shrink-0 lg:flex", children: [_jsx(MediaFoldersNav, { className: "w-(--media-folders-width) border-r", folders: store.folders, currentFolderId: store.currentFolderId, counts: store.folderCounts, rootCount: store.folderCounts.get(ROOT_FOLDER_ID) ?? 0, onNavigate: store.navigateTo, onNewFolder: () => setNewFolderOpen(true), canCreate: canCreate }), _jsx(ResizeHandle, { ...foldersPanel.handleProps, edge: "right", label: intl.formatMessage(messages.resizeNav) })] }), _jsx("div", { className: "min-w-0 flex-1 overflow-auto", children: _jsxs("div", { className: "p-4 sm:p-6", children: [_jsxs("div", { className: "mb-4", children: [_jsx("h1", { className: "text-2xl font-semibold tracking-[-0.01em]", children: store.currentFolder
350
352
  ? store.currentFolder.name
351
353
  : intl.formatMessage(messages.title) }), _jsx("p", { className: "mt-0.5 text-sm text-muted-foreground", children: intl.formatMessage(messages.subtitle, {
352
354
  count: store.total,
@@ -370,7 +372,7 @@ export function MediaLibraryPage() {
370
372
  }, onMove: () => setMoveIds(selectedIds), onDelete: () => setDeleteTarget({
371
373
  kind: 'assets',
372
374
  ids: selectedIds
373
- }), onClear: store.clearSelection, canCreate: canCreate, canUpdate: canUpdate, canDelete: canDelete })) : null, _jsx("section", { ref: gridRef, tabIndex: -1, "aria-label": intl.formatMessage(messages.assetsRegion), "aria-busy": store.isRefreshing, className: cn('transition-opacity focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring', store.isRefreshing && 'opacity-60'), children: isEmpty ? (_jsx(MediaEmptyState, { hasFilters: hasFilters, canCreate: canCreate, onUpload: () => setUploadOpen(true), onClearFilters: () => {
375
+ }), onClear: store.clearSelection, canCreate: canCreate, canUpdate: canUpdate, canDelete: canDelete })) : null, _jsx("section", { ref: gridRef, tabIndex: -1, "aria-label": intl.formatMessage(messages.assetsRegion), "aria-busy": store.isRefreshing, className: cn('transition-opacity duration-(--ds-duration-base) motion-reduce:transition-none focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring', store.isRefreshing && 'opacity-60 delay-150'), children: isEmpty ? (_jsx(MediaEmptyState, { hasFilters: hasFilters, canCreate: canCreate, onUpload: () => setUploadOpen(true), onClearFilters: () => {
374
376
  store.setSearch('');
375
377
  store.setKindFilter('all');
376
378
  } })) : (_jsx(MediaGrid, { folders: visibleFolders, assets: store.visibleAssets, folderCounts: store.folderCounts, selectedIds: store.selectedIds, onOpenFolder: store.navigateTo, onRenameFolder: handleRenameFolder, onDeleteFolder: handleDeleteFolder, onToggleSelect: store.toggleSelect, onAssetAction: handleAssetAction, canCreate: canCreate, canUpdate: canUpdate, canDelete: canDelete })) }), store.total > 0 ? (_jsx(MediaPagination, { page: store.page, pageCount: store.pageCount, pageSize: store.pageSize, total: store.total, onPageChange: store.setPage, onPageSizeChange: store.setPageSize })) : null] }) })] }), _jsx(Drawer, { direction: "left", open: navOpen, onOpenChange: setNavOpen, shouldScaleBackground: false, children: _jsxs(DrawerContent, { className: "data-[vaul-drawer-direction=left]:w-72", children: [_jsx(DrawerTitle, { className: "sr-only", children: intl.formatMessage(messages.navTitle) }), _jsx(MediaFoldersNav, { className: "w-full", folders: store.folders, currentFolderId: store.currentFolderId, counts: store.folderCounts, rootCount: store.folderCounts.get(ROOT_FOLDER_ID) ?? 0, onNavigate: (folderId) => {
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../src/lib/utils/mediaPlugin/index.tsx"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,2BAA2B,CAAC;AA6C7D;;;GAGG;AACH,MAAM,MAAM,gBAAgB,GAAG,WAAW,CAAC;AAE3C;;;;;;GAMG;AACH,wBAAgB,WAAW,IAAI,gBAAgB,CAqJ9C"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../src/lib/utils/mediaPlugin/index.tsx"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,2BAA2B,CAAC;AA4C7D;;;GAGG;AACH,MAAM,MAAM,gBAAgB,GAAG,WAAW,CAAC;AAE3C;;;;;;GAMG;AACH,wBAAgB,WAAW,IAAI,gBAAgB,CAqJ9C"}
@@ -1,5 +1,6 @@
1
1
  import { jsx as _jsx } from "react/jsx-runtime";
2
- import { Suspense, lazy } from 'react';
2
+ import { Suspense } from 'react';
3
+ import { lazyPage } from '@orthacms/utils-admin';
3
4
  import { WORKSPACE_ROUTE_SLOT, WORKSPACE_NAV_SLOT } from '@orthacms/workspaces-admin';
4
5
  import { CONTENT_FIELD_TYPE, ENTRY_PRESAVE_SLOT, ENTRY_TAB, ENTRY_TAB_SLOT } from '@orthacms/content-admin';
5
6
  import { WYSIWYG_MEDIA_SLOT } from '@orthacms/wysiwyg-admin';
@@ -15,9 +16,7 @@ import { WysiwygLibrarySource } from '../../components/WysiwygLibrarySource';
15
16
  import { WysiwygUploadSource } from '../../components/WysiwygUploadSource';
16
17
  import { MEDIA_PRESAVE_ID, usePendingMediaUploads } from '../../hooks/usePendingMediaUploads';
17
18
  import { MEDIA_READ } from '../../constants';
18
- const MediaLibraryPage = lazy(() => import('../../pages/MediaLibraryPage').then((module) => ({
19
- default: module.MediaLibraryPage
20
- })));
19
+ const MediaLibraryPage = lazyPage(() => import('../../pages/MediaLibraryPage'), 'MediaLibraryPage');
21
20
  /** The Media tab applies to any type that declares a media field. */
22
21
  function hasMediaField(schema) {
23
22
  return schema.fields.some((field) => field.type === CONTENT_FIELD_TYPE.Media);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@orthacms/media-admin",
3
- "version": "0.10.2",
3
+ "version": "0.11.0",
4
4
  "description": "@orthacms/media-admin — part of Ortha CMS.",
5
5
  "license": "MIT",
6
6
  "homepage": "https://orthacms.com",
@@ -25,15 +25,15 @@
25
25
  "dist"
26
26
  ],
27
27
  "dependencies": {
28
- "@orthacms/bootstrap-admin": "^0.10.2",
29
- "@orthacms/content-admin": "^0.10.2",
30
- "@orthacms/content-domain": "^0.10.2",
31
- "@orthacms/design-system": "^0.10.2",
32
- "@orthacms/identity-admin": "^0.10.2",
33
- "@orthacms/insights-admin": "^0.10.2",
34
- "@orthacms/utils-admin": "^0.10.2",
35
- "@orthacms/workspaces-admin": "^0.10.2",
36
- "@orthacms/wysiwyg-admin": "^0.10.2",
28
+ "@orthacms/bootstrap-admin": "^0.11.0",
29
+ "@orthacms/content-admin": "^0.11.0",
30
+ "@orthacms/content-domain": "^0.11.0",
31
+ "@orthacms/design-system": "^0.11.0",
32
+ "@orthacms/identity-admin": "^0.11.0",
33
+ "@orthacms/insights-admin": "^0.11.0",
34
+ "@orthacms/utils-admin": "^0.11.0",
35
+ "@orthacms/workspaces-admin": "^0.11.0",
36
+ "@orthacms/wysiwyg-admin": "^0.11.0",
37
37
  "@tanstack/react-query": "^5.0.0",
38
38
  "lucide-react": "^1.17.0",
39
39
  "react-intl": "^7.0.0",