@murumets-ee/media 0.70.0 → 0.72.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.
- package/dist/admin.d.mts.map +1 -1
- package/dist/admin.mjs +1 -1
- package/dist/backfill-D2Ubblh3.mjs +2 -0
- package/dist/backfill-D2Ubblh3.mjs.map +1 -0
- package/dist/backfill-trigger-BFL1L7_j.mjs +2 -0
- package/dist/backfill-trigger-BFL1L7_j.mjs.map +1 -0
- package/dist/backlog-D-H5VUQA.mjs +2 -0
- package/dist/backlog-D-H5VUQA.mjs.map +1 -0
- package/dist/client.d.mts +55 -7
- package/dist/client.d.mts.map +1 -1
- package/dist/client.mjs +1 -1
- package/dist/client.mjs.map +1 -1
- package/dist/crop-editor.d.mts +60 -0
- package/dist/crop-editor.d.mts.map +1 -0
- package/dist/crop-editor.mjs +2 -0
- package/dist/crop-editor.mjs.map +1 -0
- package/dist/crop-rect-B-AJ2pWm.mjs +2 -0
- package/dist/crop-rect-B-AJ2pWm.mjs.map +1 -0
- package/dist/crop-rect-C-SbjoxK.d.mts +59 -0
- package/dist/crop-rect-C-SbjoxK.d.mts.map +1 -0
- package/dist/crop.d.mts +3 -0
- package/dist/crop.mjs +1 -0
- package/dist/definitions-LJpgrdxd.mjs +2 -0
- package/dist/definitions-LJpgrdxd.mjs.map +1 -0
- package/dist/deps-CzHEhyJK.mjs +2 -0
- package/dist/deps-CzHEhyJK.mjs.map +1 -0
- package/dist/derive-media-type-C38ZA_Cl.mjs +2 -0
- package/dist/derive-media-type-C38ZA_Cl.mjs.map +1 -0
- package/dist/en-A8YzaZ51.mjs +2 -0
- package/dist/en-A8YzaZ51.mjs.map +1 -0
- package/dist/entity-CsDdjKz6.mjs +2 -0
- package/dist/entity-CsDdjKz6.mjs.map +1 -0
- package/dist/et-CXvu8U31.mjs +2 -0
- package/dist/et-CXvu8U31.mjs.map +1 -0
- package/dist/focal-point-BWhtJzef.mjs +2 -0
- package/dist/focal-point-BWhtJzef.mjs.map +1 -0
- package/dist/focal-point-C_0drV03.d.mts +125 -0
- package/dist/focal-point-C_0drV03.d.mts.map +1 -0
- package/dist/generate-variants-zyPjaGcy.mjs +2 -0
- package/dist/generate-variants-zyPjaGcy.mjs.map +1 -0
- package/dist/i18n.mjs +1 -1
- package/dist/i18n.mjs.map +1 -1
- package/dist/image-styles-settings.d.mts +8 -1
- package/dist/image-styles-settings.d.mts.map +1 -1
- package/dist/image-styles-settings.mjs +1 -1
- package/dist/image-styles-settings.mjs.map +1 -1
- package/dist/image-styles.d.mts +35 -21
- package/dist/image-styles.d.mts.map +1 -1
- package/dist/image-styles.mjs +1 -2
- package/dist/image-styles.mjs.map +1 -1
- package/dist/index.d.mts +16 -42
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +1 -1
- package/dist/index.mjs.map +1 -1
- package/dist/media-config-DmcTxuDM.mjs +2 -0
- package/dist/media-config-DmcTxuDM.mjs.map +1 -0
- package/dist/owned-delete-Cn_KFFjS.mjs +2 -0
- package/dist/owned-delete-Cn_KFFjS.mjs.map +1 -0
- package/dist/picker.d.mts +108 -99
- package/dist/picker.d.mts.map +1 -1
- package/dist/picker.mjs +1 -2
- package/dist/picker.mjs.map +1 -1
- package/dist/plugin.d.mts +2 -1
- package/dist/plugin.d.mts.map +1 -1
- package/dist/plugin.mjs +1 -1
- package/dist/plugin.mjs.map +1 -1
- package/dist/process-image-Deedtkh4.mjs +2 -0
- package/dist/process-image-Deedtkh4.mjs.map +1 -0
- package/dist/processing.d.mts +201 -7
- package/dist/processing.d.mts.map +1 -1
- package/dist/processing.mjs +1 -1
- package/dist/public-resolver.d.mts +14 -1
- package/dist/public-resolver.d.mts.map +1 -1
- package/dist/public-resolver.mjs +1 -1
- package/dist/public-resolver.mjs.map +1 -1
- package/dist/query-client.d.mts +1 -1
- package/dist/query-client.mjs +1 -1
- package/dist/regenerate-variants-Cp3sNHkT.mjs +2 -0
- package/dist/regenerate-variants-Cp3sNHkT.mjs.map +1 -0
- package/dist/register-BSGkELj0.mjs +2 -0
- package/dist/register-BSGkELj0.mjs.map +1 -0
- package/dist/resolve-image-styles-pf7Nd3Zf.mjs +2 -0
- package/dist/resolve-image-styles-pf7Nd3Zf.mjs.map +1 -0
- package/dist/routes-BJ23Cp0g.mjs +2 -0
- package/dist/routes-BJ23Cp0g.mjs.map +1 -0
- package/dist/ru-Dj-ax8s_.mjs +2 -0
- package/dist/ru-Dj-ax8s_.mjs.map +1 -0
- package/dist/schedule-DREjg2ji.mjs +2 -0
- package/dist/schedule-DREjg2ji.mjs.map +1 -0
- package/dist/shapes-40dHtTb-.d.mts +90 -0
- package/dist/shapes-40dHtTb-.d.mts.map +1 -0
- package/dist/shapes-BlKW-0C1.mjs +2 -0
- package/dist/shapes-BlKW-0C1.mjs.map +1 -0
- package/dist/slot-CY4isqmN.mjs +2 -0
- package/dist/slot-CY4isqmN.mjs.map +1 -0
- package/dist/types-CgkJF5dc.d.mts +261 -0
- package/dist/types-CgkJF5dc.d.mts.map +1 -0
- package/dist/types-DwzblZfW.d.mts +97 -0
- package/dist/types-DwzblZfW.d.mts.map +1 -0
- package/dist/variant-key-Cki4n-42.mjs +2 -0
- package/dist/variant-key-Cki4n-42.mjs.map +1 -0
- package/dist/variant-plan-DV8hsMdz.mjs +2 -0
- package/dist/variant-plan-DV8hsMdz.mjs.map +1 -0
- package/package.json +25 -8
- package/dist/client-YlSoGADl.mjs +0 -2
- package/dist/client-YlSoGADl.mjs.map +0 -1
- package/dist/en-0YnQ_Qz-.mjs +0 -2
- package/dist/en-0YnQ_Qz-.mjs.map +0 -1
- package/dist/entity-Caba_NFw.mjs +0 -2
- package/dist/entity-Caba_NFw.mjs.map +0 -1
- package/dist/entity-fxw-Qywj.mjs +0 -2
- package/dist/entity-fxw-Qywj.mjs.map +0 -1
- package/dist/et-BIxGYWVK.mjs +0 -2
- package/dist/et-BIxGYWVK.mjs.map +0 -1
- package/dist/image-styles-settings-ClIA2MJI.mjs +0 -2
- package/dist/image-styles-settings-ClIA2MJI.mjs.map +0 -1
- package/dist/plugin-BkEED6qj.mjs +0 -2
- package/dist/plugin-BkEED6qj.mjs.map +0 -1
- package/dist/process-image-DYDTMGUJ.mjs +0 -2
- package/dist/process-image-DYDTMGUJ.mjs.map +0 -1
- package/dist/regenerate-variants-BysyeIoA.mjs +0 -2
- package/dist/regenerate-variants-BysyeIoA.mjs.map +0 -1
- package/dist/regenerate-variants-sit6LbUo.mjs +0 -2
- package/dist/regenerate-variants-sit6LbUo.mjs.map +0 -1
- package/dist/resolve-image-styles-BzSRuXUs.mjs +0 -2
- package/dist/resolve-image-styles-BzSRuXUs.mjs.map +0 -1
- package/dist/resolve-image-styles-iN9JbZYf.mjs +0 -2
- package/dist/resolve-image-styles-iN9JbZYf.mjs.map +0 -1
- package/dist/routes-RCy0SYsK.mjs +0 -2
- package/dist/routes-RCy0SYsK.mjs.map +0 -1
- package/dist/ru-UUTBfsMF.mjs +0 -2
- package/dist/ru-UUTBfsMF.mjs.map +0 -1
- package/dist/types-1idCpe1k.d.mts +0 -160
- package/dist/types-1idCpe1k.d.mts.map +0 -1
- package/dist/usage-CVAqkS6h.mjs +0 -2
- package/dist/usage-CVAqkS6h.mjs.map +0 -1
- package/dist/variant-key-CLlac3_1.mjs +0 -2
- package/dist/variant-key-CLlac3_1.mjs.map +0 -1
- package/dist/variant-key-JBTJXPL1.mjs +0 -2
- package/dist/variant-key-JBTJXPL1.mjs.map +0 -1
package/dist/picker.d.mts
CHANGED
|
@@ -1,100 +1,7 @@
|
|
|
1
|
-
|
|
1
|
+
import { a as MediaPickerMode, i as MediaPickerListResult, n as MediaPickerClassNames, o as MediaPickerProps, r as MediaPickerItem, t as MediaPickerCallbacks } from "./types-DwzblZfW.mjs";
|
|
2
2
|
import { LucideIcon } from "lucide-react";
|
|
3
3
|
import { ReactNode } from "react";
|
|
4
4
|
|
|
5
|
-
//#region src/picker/types.d.ts
|
|
6
|
-
/** Lightweight media item for the picker (no server-only deps) */
|
|
7
|
-
interface MediaPickerItem {
|
|
8
|
-
id: string;
|
|
9
|
-
title: string | null;
|
|
10
|
-
alt: string | null;
|
|
11
|
-
filename: string;
|
|
12
|
-
mimeType: string;
|
|
13
|
-
size: number;
|
|
14
|
-
width: number | null;
|
|
15
|
-
height: number | null;
|
|
16
|
-
mediaType: 'image' | 'video' | 'audio' | 'document' | 'other';
|
|
17
|
-
url: string;
|
|
18
|
-
}
|
|
19
|
-
/** Paginated result from fetchMedia callback */
|
|
20
|
-
interface MediaPickerListResult {
|
|
21
|
-
items: MediaPickerItem[];
|
|
22
|
-
total: number;
|
|
23
|
-
/**
|
|
24
|
-
* False when the server reports storage is not configured (missing
|
|
25
|
-
* env vars etc.). UI renders a banner with `reason` instead of the
|
|
26
|
-
* upload zone + grid. Omitted/true when everything is wired.
|
|
27
|
-
*/
|
|
28
|
-
configured?: boolean;
|
|
29
|
-
/** Human-readable explanation when `configured === false`. */
|
|
30
|
-
reason?: string;
|
|
31
|
-
}
|
|
32
|
-
/** Callbacks the consumer provides — framework-agnostic */
|
|
33
|
-
interface MediaPickerCallbacks {
|
|
34
|
-
/** Fetch media items with filtering and pagination */
|
|
35
|
-
fetchMedia: (options: {
|
|
36
|
-
search?: string | undefined;
|
|
37
|
-
mediaType?: string | undefined;
|
|
38
|
-
limit: number;
|
|
39
|
-
offset: number;
|
|
40
|
-
}) => Promise<MediaPickerListResult>;
|
|
41
|
-
/** Upload a file and return the created media item */
|
|
42
|
-
uploadMedia: (file: File) => Promise<MediaPickerItem>;
|
|
43
|
-
}
|
|
44
|
-
/** Selection mode */
|
|
45
|
-
type MediaPickerMode = 'single' | 'multiple';
|
|
46
|
-
/** Per-element class overrides for MediaPicker */
|
|
47
|
-
interface MediaPickerClassNames {
|
|
48
|
-
overlay?: string;
|
|
49
|
-
content?: string;
|
|
50
|
-
header?: string;
|
|
51
|
-
title?: string;
|
|
52
|
-
toolbar?: string;
|
|
53
|
-
searchInput?: string;
|
|
54
|
-
filterTabs?: string;
|
|
55
|
-
grid?: string;
|
|
56
|
-
card?: string;
|
|
57
|
-
cardSelected?: string;
|
|
58
|
-
cardImage?: string;
|
|
59
|
-
cardLabel?: string;
|
|
60
|
-
uploadZone?: string;
|
|
61
|
-
uploadZoneActive?: string;
|
|
62
|
-
footer?: string;
|
|
63
|
-
confirmButton?: string;
|
|
64
|
-
cancelButton?: string;
|
|
65
|
-
loading?: string;
|
|
66
|
-
empty?: string;
|
|
67
|
-
}
|
|
68
|
-
/** Props for the MediaPicker dialog */
|
|
69
|
-
interface MediaPickerProps {
|
|
70
|
-
/** Whether the dialog is open */
|
|
71
|
-
open: boolean;
|
|
72
|
-
/** Called when open state changes */
|
|
73
|
-
onOpenChange: (open: boolean) => void;
|
|
74
|
-
/** Called when user confirms selection */
|
|
75
|
-
onSelect: (items: MediaPickerItem[]) => void;
|
|
76
|
-
/** Selection mode (default: 'single') */
|
|
77
|
-
mode?: MediaPickerMode | undefined;
|
|
78
|
-
/** MIME patterns for upload restriction (e.g., ['image/*', 'video/*']) — passed to file input */
|
|
79
|
-
accept?: string[] | undefined;
|
|
80
|
-
/** Filter browse results to a specific media classification (e.g., 'image', 'video') */
|
|
81
|
-
mediaType?: string | undefined;
|
|
82
|
-
/** Maximum items selectable in multi mode */
|
|
83
|
-
maxSelect?: number | undefined;
|
|
84
|
-
/** Currently selected item IDs (for pre-selection) */
|
|
85
|
-
selectedIds?: string[] | undefined;
|
|
86
|
-
/** Dialog title (default: 'Select Media') */
|
|
87
|
-
title?: string | undefined;
|
|
88
|
-
/** Dialog description for screen readers (optional) */
|
|
89
|
-
description?: string | undefined;
|
|
90
|
-
/** Per-element class overrides */
|
|
91
|
-
classNames?: MediaPickerClassNames | undefined;
|
|
92
|
-
/** Additional className for the dialog content */
|
|
93
|
-
className?: string | undefined;
|
|
94
|
-
/** Children rendered in dialog footer (extra actions) */
|
|
95
|
-
children?: ReactNode | undefined;
|
|
96
|
-
}
|
|
97
|
-
//#endregion
|
|
98
5
|
//#region src/picker/admin-callbacks.d.ts
|
|
99
6
|
/** Extended callbacks with getMediaUrl (for editor thumbnail previews) */
|
|
100
7
|
type AdminMediaCallbacks = MediaPickerCallbacks & {
|
|
@@ -119,21 +26,103 @@ type AdminMediaCallbacks = MediaPickerCallbacks & {
|
|
|
119
26
|
*/
|
|
120
27
|
declare function createAdminMediaCallbacks(apiBasePath?: string): AdminMediaCallbacks;
|
|
121
28
|
//#endregion
|
|
122
|
-
//#region src/
|
|
29
|
+
//#region src/grid/media-card.d.ts
|
|
30
|
+
/**
|
|
31
|
+
* A structural copy of `@murumets-ee/media`'s own `MediaType`, not an
|
|
32
|
+
* import — same reason `media-type-icon.ts`'s `NonImageMediaType` isn't one.
|
|
33
|
+
* This module ships from the `./picker` tsdown entry (re-exported by
|
|
34
|
+
* `picker/index.ts`), a build SEPARATE from the root `index.ts` entry that
|
|
35
|
+
* already exports `MediaType` by name; importing (and thereby re-exporting,
|
|
36
|
+
* via `MediaCardProps`) it here too duplicates its declaration across two
|
|
37
|
+
* `.d.mts` files (`verify:dts`, #767) the moment this type reaches the
|
|
38
|
+
* entry's own public surface.
|
|
39
|
+
*/
|
|
40
|
+
type MediaCardMediaType = 'image' | 'video' | 'audio' | 'document' | 'other';
|
|
41
|
+
/**
|
|
42
|
+
* One tile: a thumbnail (or a typed icon for anything that is not an image),
|
|
43
|
+
* a hover title overlay, and a selection checkmark.
|
|
44
|
+
*
|
|
45
|
+
* Promoted out of `picker/` (entity-table PR06 §2) — this is the whole point
|
|
46
|
+
* of the promotion. `<MediaPicker>`'s `MediaGrid` and the admin-ui entity
|
|
47
|
+
* list's `grid` layout renderer both draw tiles through this ONE component,
|
|
48
|
+
* so a feature added to one reaches both rather than diverging the way
|
|
49
|
+
* WordPress's Backbone grid and `WP_List_Table` did (D007's cautionary tale).
|
|
50
|
+
*
|
|
51
|
+
* Decoupled from `MediaPickerItem` on purpose: the entity list's grid layout
|
|
52
|
+
* has no `MediaPickerItem` to hand it — it has a TanStack row and a
|
|
53
|
+
* `ViewGridOptions`-resolved field name — so the props here are the minimal
|
|
54
|
+
* shape both callers can build without synthesising fake data for fields
|
|
55
|
+
* neither has (a picker item's `size`/`width`/`height`, an entity row's MIME
|
|
56
|
+
* type).
|
|
57
|
+
*/
|
|
123
58
|
interface MediaCardProps {
|
|
124
|
-
|
|
59
|
+
/** Resolved thumbnail/asset URL. `null` (or a non-`'image'` `mediaType`) draws the typed icon instead. */
|
|
60
|
+
url: string | null;
|
|
61
|
+
/** The tile's accessible name and its hover-overlay label. */
|
|
62
|
+
title: string;
|
|
63
|
+
/**
|
|
64
|
+
* The image's OWN alt text, when the caller has one — an editor-authored
|
|
65
|
+
* field distinct from `title` (a display name most often equal to the
|
|
66
|
+
* filename). Falls back to `title` only when omitted (`undefined`) or
|
|
67
|
+
* `null` — a deliberately EMPTY string (`''`, the WCAG decorative-image
|
|
68
|
+
* marker) is preserved, not overridden. Review finding, entity-table PR06:
|
|
69
|
+
* the promotion had collapsed this into `title` and silently dropped a
|
|
70
|
+
* screen-reader-meaningful alt text down to the display name; a follow-up
|
|
71
|
+
* critique pass then caught the first fix using `||` (which clobbers `''`
|
|
72
|
+
* the same way) instead of `??`.
|
|
73
|
+
*/
|
|
74
|
+
alt?: string | null | undefined;
|
|
75
|
+
/**
|
|
76
|
+
* The item's classification. `'image'` draws `url` as an `<img>`;
|
|
77
|
+
* anything else — including `undefined`, an entity with no classification
|
|
78
|
+
* field at all — draws a typed (or generic) icon, never a broken `<img>`.
|
|
79
|
+
*/
|
|
80
|
+
mediaType: MediaCardMediaType | undefined;
|
|
125
81
|
isSelected: boolean;
|
|
126
82
|
onToggle: () => void;
|
|
83
|
+
/**
|
|
84
|
+
* Renders the tile as a real `<a href>` instead of a select button — the
|
|
85
|
+
* admin-ui entity list's grid layout (entity-table D004 ported to a card:
|
|
86
|
+
* the tile is the row's link, same as the table's title cell) sets this so
|
|
87
|
+
* cmd/ctrl-click, middle-click and "open in new tab" all work, exactly the
|
|
88
|
+
* capabilities S4 requires of the table. `onToggle` and `isSelected` still
|
|
89
|
+
* apply — the caller's OWN checkbox overlay (drawn outside this component,
|
|
90
|
+
* at a higher z-index) is what fires `onToggle`, not a click on the tile.
|
|
91
|
+
*
|
|
92
|
+
* Omitted (the picker's own use, `<MediaGrid>`) keeps the whole tile a
|
|
93
|
+
* select button, unchanged from before the promotion.
|
|
94
|
+
*/
|
|
95
|
+
href?: string | undefined;
|
|
96
|
+
/**
|
|
97
|
+
* Suppresses this tile's own checkmark badge — driven by whether the
|
|
98
|
+
* CALLER is drawing a DIFFERENT overlay in the same top-right corner, not
|
|
99
|
+
* by `href`. Critique finding, entity-table PR06: the first cut of the
|
|
100
|
+
* overlay-collision fix keyed this off `!!href`, which only happens to
|
|
101
|
+
* equal "does the caller draw an actions overlay here" for today's one
|
|
102
|
+
* caller (Media, where `editHref` and `allowDelete` are both always on).
|
|
103
|
+
* The moment an entity reaches the grid through the generic list page with
|
|
104
|
+
* a permission-gated `editHref` (so `href` can be `undefined`) while still
|
|
105
|
+
* allowing delete (so an actions overlay IS drawn), `!!href` goes false
|
|
106
|
+
* and the badge reappears under the menu it was meant to stay clear of.
|
|
107
|
+
* `EntityGridLayout` passes the caller's own truth — whether it rendered
|
|
108
|
+
* an `_actions` cell for this row — instead of re-deriving it from `href`.
|
|
109
|
+
*/
|
|
110
|
+
hideSelectionBadge?: boolean | undefined;
|
|
127
111
|
classNames?: MediaPickerClassNames | undefined;
|
|
128
112
|
}
|
|
129
113
|
declare function MediaCard({
|
|
130
|
-
|
|
114
|
+
url,
|
|
115
|
+
title,
|
|
116
|
+
alt,
|
|
117
|
+
mediaType,
|
|
131
118
|
isSelected,
|
|
132
119
|
onToggle,
|
|
120
|
+
href,
|
|
121
|
+
hideSelectionBadge,
|
|
133
122
|
classNames
|
|
134
123
|
}: MediaCardProps): import("react/jsx-runtime").JSX.Element;
|
|
135
124
|
//#endregion
|
|
136
|
-
//#region src/
|
|
125
|
+
//#region src/grid/media-grid.d.ts
|
|
137
126
|
interface MediaGridProps {
|
|
138
127
|
items: MediaPickerItem[];
|
|
139
128
|
selected: Set<string>;
|
|
@@ -145,6 +134,12 @@ interface MediaGridProps {
|
|
|
145
134
|
onPageChange: (offset: number) => void;
|
|
146
135
|
classNames?: MediaPickerClassNames | undefined;
|
|
147
136
|
}
|
|
137
|
+
/**
|
|
138
|
+
* The `<MediaPicker>` dialog's own grid — its pagination (offset/limit, a
|
|
139
|
+
* Prev/Next footer) is the picker's, not the entity list's. The TILE it
|
|
140
|
+
* draws is the promoted, shared `MediaCard` (entity-table PR06 §2); this
|
|
141
|
+
* wrapper's job is adapting `MediaPickerItem[]` to that tile's generic props.
|
|
142
|
+
*/
|
|
148
143
|
declare function MediaGrid({
|
|
149
144
|
items,
|
|
150
145
|
selected,
|
|
@@ -204,6 +199,20 @@ type NonImageMediaType = 'video' | 'audio' | 'document' | 'other';
|
|
|
204
199
|
declare const NON_IMAGE_MEDIA_TYPE_ICONS: Record<NonImageMediaType, LucideIcon>;
|
|
205
200
|
/** The icon for a non-image media type, or `null` for `'image'` (render the asset itself). */
|
|
206
201
|
declare function mediaTypeIcon(mediaType: 'image' | NonImageMediaType): LucideIcon | null;
|
|
202
|
+
/**
|
|
203
|
+
* The icon for a MIME type — the admin list's MIME column, where the string
|
|
204
|
+
* `video/mp4` is replaced by a film (entity-table D008's `mimeType` display).
|
|
205
|
+
*
|
|
206
|
+
* Unlike {@link mediaTypeIcon}, an image gets one too: that function returns
|
|
207
|
+
* `null` for `'image'` because its callers draw the ASSET instead, and a column
|
|
208
|
+
* that shows a type has no asset to draw. The classification is
|
|
209
|
+
* `deriveMediaType` — the rule the upload stored the row's `mediaType` by — so
|
|
210
|
+
* the icon cannot disagree with the type column beside it.
|
|
211
|
+
*
|
|
212
|
+
* Takes a plain string and returns a `LucideIcon`, so `MediaType` is not named
|
|
213
|
+
* in this entry's public API (see the note above `NonImageMediaType`).
|
|
214
|
+
*/
|
|
215
|
+
declare function mimeTypeIcon(mimeType: string): LucideIcon;
|
|
207
216
|
//#endregion
|
|
208
217
|
//#region src/picker/provider.d.ts
|
|
209
218
|
interface MediaPickerProviderProps extends MediaPickerCallbacks {
|
|
@@ -263,5 +272,5 @@ declare function UploadZone({
|
|
|
263
272
|
classNames
|
|
264
273
|
}: UploadZoneProps): import("react/jsx-runtime").JSX.Element;
|
|
265
274
|
//#endregion
|
|
266
|
-
export { type AdminMediaCallbacks, MediaCard, MediaGrid, MediaPicker, type MediaPickerCallbacks, type MediaPickerClassNames, type MediaPickerItem, type MediaPickerListResult, type MediaPickerMode, type MediaPickerProps, MediaPickerProvider, NON_IMAGE_MEDIA_TYPE_ICONS, SearchBar, UploadZone, createAdminMediaCallbacks, mediaTypeIcon, useMediaPicker };
|
|
275
|
+
export { type AdminMediaCallbacks, MediaCard, type MediaCardProps, MediaGrid, MediaPicker, type MediaPickerCallbacks, type MediaPickerClassNames, type MediaPickerItem, type MediaPickerListResult, type MediaPickerMode, type MediaPickerProps, MediaPickerProvider, NON_IMAGE_MEDIA_TYPE_ICONS, SearchBar, UploadZone, createAdminMediaCallbacks, mediaTypeIcon, mimeTypeIcon, useMediaPicker };
|
|
267
276
|
//# sourceMappingURL=picker.d.mts.map
|
package/dist/picker.d.mts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"picker.d.mts","names":[],"sources":["../src/picker/
|
|
1
|
+
{"version":3,"file":"picker.d.mts","names":[],"sources":["../src/picker/admin-callbacks.ts","../src/grid/media-card.tsx","../src/grid/media-grid.tsx","../src/picker/media-picker.tsx","../src/picker/media-type-icon.ts","../src/picker/provider.tsx","../src/picker/search-bar.tsx","../src/picker/upload-zone.tsx"],"mappings":";;;;;;KAaY,mBAAA,GAAsB,oBAAA;EAChC,WAAA,GAAc,EAAA,aAAe,OAAO;AAAA;AAAA;AAoBtC;;;;AAA0F;;;;AC/B9B;;;;AAYrC;AAmBvB;;;ADpBsC,iBAoBtB,yBAAA,CAA0B,WAAA,YAA6B,mBAAmB;;;;;;;AArB1F;;;;;;KCEK,kBAAA;;;ADDiC;AAoBtC;;;;AAA0F;;;;AC/B9B;;;;AAYrC;AAmBvB;UAAiB,cAAA;;EAEf,GAAA;EAAA;EAEA,KAAA;EAYA;;;;;;;;;;AAqCkC;EArClC,GAAA;EAwCuB;;;;;EAlCvB,SAAA,EAAW,kBAAA;EACX,UAAA;EACA,QAAA;EAuCA;;;;;;;;;;;;EA1BA,IAAA;EAuBA;;;;;;;;;;;;;;EARA,kBAAA;EACA,UAAA,GAAa,qBAAqB;AAAA;AAAA,iBAGpB,SAAA,CAAA;EACd,GAAA;EACA,KAAA;EACA,GAAA;EACA,SAAA;EACA,UAAA;EACA,QAAA;EACA,IAAA;EACA,kBAAA;EACA;AAAA,GACC,cAAA,+BAAc,GAAA,CAAA,OAAA;;;UChGP,cAAA;EACR,KAAA,EAAO,eAAA;EACP,QAAA,EAAU,GAAA;EACV,QAAA,GAAW,IAAA,EAAM,eAAA;EACjB,SAAA;EACA,KAAA;EACA,MAAA;EACA,KAAA;EACA,YAAA,GAAe,MAAA;EACf,UAAA,GAAa,qBAAA;AAAA;;;AFCuB;AAoBtC;;;iBEZgB,SAAA,CAAA;EACd,KAAA;EACA,QAAA;EACA,QAAA;EACA,SAAA;EACA,KAAA;EACA,MAAA;EACA,KAAA;EACA,YAAA;EACA;AAAA,GACC,cAAA,+BAAc,GAAA,CAAA,OAAA;;;iBCgCD,WAAA,CAAA;EACd,IAAA;EACA,YAAA;EACA,QAAA;EACA,IAAA;EACA,MAAA;EACA,SAAA;EACA,SAAA;EACA,WAAA;EACA,KAAA;EACA,WAAA;EACA,UAAA;EACA,SAAA;EACA;AAAA,GACC,gBAAA,+BAAgB,GAAA,CAAA,OAAA;;;;;;;AHjEnB;;;;;;;;;AACsC;AAoBtC;;KIfK,iBAAA;;AJeqF;;;;AC/B9B;;;;cG2B/C,0BAAA,EAA4B,MAAA,CAAO,iBAAA,EAAmB,UAAA;AHInE;AAAA,iBGIgB,aAAA,CAAc,SAAA,YAAqB,iBAAA,GAAoB,UAAU;;;;;;;;;;;;;;iBAiBjE,YAAA,CAAa,QAAA,WAAmB,UAAU;;;UClDzC,wBAAA,SAAiC,oBAAoB;EACpE,QAAA,EAAU,SAAA;AAAA;ALOZ;;;;;;;;;AACsC;AAoBtC;;;;AArBA,iBKUgB,mBAAA,CAAA;EACd,QAAA;EACA,UAAA;EACA;AAAA,GACC,wBAAA,+BAAwB,GAAA,CAAA,OAAA;AAAA,iBASX,cAAA,CAAA,GAAkB,oBAAoB;;;UChC5C,cAAA;EACR,KAAA;EACA,QAAA,GAAW,KAAA;EACX,SAAA;EACA,iBAAA,GAAoB,IAAA;ENKS;EMH7B,MAAA;EACA,UAAA,GAAa,qBAAqB;AAAA;AAAA,iBAWpB,SAAA,CAAA;EACd,KAAA;EACA,QAAA;EACA,SAAA;EACA,iBAAA;EACA,MAAA;EACA;AAAA,GACC,cAAA,+BAAc,GAAA,CAAA,OAAA;;;UCxBP,eAAA;EACR,QAAA,GAAW,IAAA,EAAM,IAAA,KAAS,OAAA;EAC1B,WAAA;EACA,MAAA;EACA,UAAA,GAAa,qBAAA;AAAA;AAAA,iBAGC,UAAA,CAAA;EAAa,QAAA;EAAU,WAAA;EAAa,MAAA;EAAQ;AAAA,GAAc,eAAA,+BAAe,GAAA,CAAA,OAAA"}
|
package/dist/picker.mjs
CHANGED
|
@@ -1,3 +1,2 @@
|
|
|
1
|
-
"use client";
|
|
2
|
-
import{Check as e,File as t,FileText as n,Film as r,Music as i,Search as a,Upload as o,X as s}from"lucide-react";import{clsx as c}from"clsx";import{twMerge as l}from"tailwind-merge";import{Fragment as u,jsx as d,jsxs as f}from"react/jsx-runtime";import*as p from"@radix-ui/react-dialog";import*as m from"@radix-ui/react-visually-hidden";import{createContext as h,useCallback as g,useContext as _,useEffect as v,useMemo as y,useRef as b,useState as x}from"react";function S(e=`/api/admin`){let t=`${e}/media`,n=async e=>{try{let t=await e.clone().json();if(typeof t.error==`string`&&t.error.length>0)return t.error}catch{}try{let t=await e.text();if(t)return t}catch{}return`HTTP ${String(e.status)}`};return{fetchMedia:async e=>{let r=new URLSearchParams;e.search&&r.set(`search`,e.search),e.mediaType&&r.set(`mediaType`,e.mediaType),r.set(`limit`,String(e.limit)),r.set(`offset`,String(e.offset));let i=`${t}?${r.toString()}`,a=await fetch(i);if(!a.ok)throw Error(await n(a));return a.json()},uploadMedia:async e=>{let r=new FormData;r.append(`file`,e),r.append(`visibility`,`public`);let i=await fetch(t,{method:`POST`,body:r});if(!i.ok)throw Error(await n(i));return i.json()},getMediaUrl:async e=>{try{let n=await fetch(`${t}/${e}`);return n.ok?(await n.json()).url??null:null}catch{return null}}}}function C(...e){return l(c(e))}const w={video:r,audio:i,document:n,other:t};function T(e){return e===`image`?null:w[e]}function E({item:t,isSelected:n,onToggle:r,classNames:i}){let a=t.mediaType===`image`,o=T(t.mediaType);return f(`button`,{type:`button`,onClick:r,className:C(`group relative aspect-square overflow-hidden rounded-lg border-2 transition-all`,`focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 dark:focus:ring-offset-zinc-950`,n?`border-blue-500 ring-2 ring-blue-500/20`:`border-zinc-200 hover:border-zinc-300 dark:border-zinc-800 dark:hover:border-zinc-700`,i?.card,n&&i?.cardSelected),children:[a?d(`img`,{src:t.url,alt:t.alt??t.filename,className:C(`h-full w-full object-cover`,i?.cardImage),loading:`lazy`}):d(`div`,{className:`flex h-full w-full items-center justify-center bg-zinc-100 dark:bg-zinc-800`,children:o&&d(o,{className:`h-8 w-8 text-zinc-400 dark:text-zinc-500`})}),n&&d(`div`,{className:`absolute right-1.5 top-1.5 flex h-5 w-5 items-center justify-center rounded-full bg-blue-500 text-white`,children:d(e,{className:`h-3 w-3`})}),d(`div`,{className:C(`absolute inset-x-0 bottom-0 bg-gradient-to-t from-black/60 to-transparent px-2 py-1.5`,`opacity-0 transition-opacity group-hover:opacity-100`,i?.cardLabel),children:d(`p`,{className:`truncate text-xs text-white`,children:t.title??t.filename})})]})}function D({items:e,selected:t,onToggle:n,isLoading:r,total:i,offset:a,limit:o,onPageChange:s,classNames:c}){if(r)return d(`div`,{className:C(`grid grid-cols-4 gap-3 sm:grid-cols-6`,c?.grid),children:Array.from({length:12}).map((e,t)=>d(`div`,{className:C(`aspect-square animate-pulse rounded-lg bg-zinc-100 dark:bg-zinc-800`,c?.loading)},`skeleton-${t.toString()}`))});if(e.length===0)return d(`div`,{className:C(`py-12 text-center text-sm text-zinc-500 dark:text-zinc-400`,c?.empty),children:`No media found. Upload a file to get started.`});let l=Math.ceil(i/o),p=Math.floor(a/o)+1;return f(u,{children:[d(`div`,{className:C(`grid grid-cols-4 gap-3 sm:grid-cols-6`,c?.grid),children:e.map(e=>d(E,{item:e,isSelected:t.has(e.id),onToggle:()=>n(e),classNames:c},e.id))}),l>1&&f(`div`,{className:`mt-4 flex items-center justify-center gap-2`,children:[d(`button`,{type:`button`,disabled:p<=1,onClick:()=>s(a-o),className:`rounded-md border border-zinc-300 px-3 py-1 text-sm disabled:opacity-50 dark:border-zinc-700`,children:`Prev`}),f(`span`,{className:`text-sm text-zinc-500 dark:text-zinc-400`,children:[p,` / `,l]}),d(`button`,{type:`button`,disabled:p>=l,onClick:()=>s(a+o),className:`rounded-md border border-zinc-300 px-3 py-1 text-sm disabled:opacity-50 dark:border-zinc-700`,children:`Next`})]})]})}const O=h(null);function k({children:e,fetchMedia:t,uploadMedia:n}){return d(O,{value:y(()=>({fetchMedia:t,uploadMedia:n}),[t,n]),children:e})}function A(){let e=_(O);if(!e)throw Error(`useMediaPicker must be used within <MediaPickerProvider>. Wrap your admin layout with <MediaPickerProvider fetchMedia={...} uploadMedia={...}>.`);return e}const j=[{value:void 0,label:`All`},{value:`image`,label:`Images`},{value:`video`,label:`Videos`},{value:`audio`,label:`Audio`},{value:`document`,label:`Docs`}];function M({value:e,onChange:t,mediaType:n,onMediaTypeChange:r,locked:i,classNames:o}){let s=!i;return f(`div`,{className:`flex items-center gap-3`,children:[f(`div`,{className:`relative flex-1`,children:[d(a,{className:`absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-zinc-400`}),d(`input`,{type:`text`,value:e,onChange:e=>t(e.target.value),placeholder:`Search media...`,className:C(`w-full rounded-md border border-zinc-300 bg-transparent py-2 pl-10 pr-3 text-sm`,`placeholder:text-zinc-400 focus:border-blue-500 focus:outline-none focus:ring-1 focus:ring-blue-500`,`dark:border-zinc-700 dark:placeholder:text-zinc-500`,o?.searchInput)})]}),s&&d(`div`,{className:C(`flex gap-1`,o?.filterTabs),children:j.map(e=>d(`button`,{type:`button`,onClick:()=>r(e.value),className:C(`rounded-md px-3 py-1.5 text-xs font-medium transition-colors`,n===e.value?`bg-zinc-900 text-white dark:bg-zinc-100 dark:text-zinc-900`:`text-zinc-600 hover:bg-zinc-100 dark:text-zinc-400 dark:hover:bg-zinc-800`),children:e.label},e.label))})]})}function N({onUpload:e,isUploading:t,accept:n,classNames:r}){let i=b(null),[a,s]=x(!1),c=g(async t=>{t.preventDefault(),s(!1);let n=t.dataTransfer.files[0];n&&await e(n)},[e]),l=g(async t=>{let n=t.target.files?.[0];n&&(await e(n),t.target.value=``)},[e]);return f(`button`,{type:`button`,onDragOver:e=>{e.preventDefault(),s(!0)},onDragLeave:()=>s(!1),onDrop:c,onClick:()=>i.current?.click(),className:C(`mb-4 flex cursor-pointer flex-col items-center justify-center rounded-lg border-2 border-dashed px-6 py-6 transition-colors`,a?`border-blue-500 bg-blue-50 dark:bg-blue-950/20`:`border-zinc-300 hover:border-zinc-400 dark:border-zinc-700 dark:hover:border-zinc-600`,r?.uploadZone,a&&r?.uploadZoneActive),children:[d(o,{className:`mb-2 h-6 w-6 text-zinc-400 dark:text-zinc-500`}),d(`p`,{className:`text-sm text-zinc-600 dark:text-zinc-400`,children:t?`Uploading...`:`Drop a file here or click to upload`}),d(`input`,{ref:i,type:`file`,accept:n?.join(`,`),onChange:l,className:`hidden`})]})}const P=new Set([`image`,`video`,`audio`]);function F(e){return e!==void 0&&P.has(e)?[`${e}/*`]:void 0}const I=`file`;function ee(e,t){return e===void 0||t.type===``?!0:e===I?!t.type.startsWith(`image/`)&&!t.type.startsWith(`video/`):P.has(e)?t.type.startsWith(`${e}/`):!0}function L(e,t){if(e===void 0||t.mediaType===e)return null;let n=t.title??t.filename;return e===I?t.mediaType!==`image`&&t.mediaType!==`video`?null:`This field takes a document or other file — “${n}” is ${R(t.mediaType)}.`:`This field takes ${e} files — “${n}” is ${t.mediaType===`other`?`not one`:R(t.mediaType)}.`}function R(e){return/^[aeiou]/.test(e)?`an ${e}`:`a ${e}`}function z({open:e,onOpenChange:t,onSelect:n,mode:r=`single`,accept:i,mediaType:a,maxSelect:o,selectedIds:c=[],title:l=`Select Media`,description:h,classNames:_,className:y,children:S}){let{fetchMedia:w,uploadMedia:T}=A(),[E,O]=x([]),[k,j]=x(0),[P,R]=x(()=>new Set(c)),[z,B]=x(``),[te,ne]=x(void 0),V=a??te,H=a===I?void 0:V,re=i??F(a),[ie,U]=x(!1),[ae,W]=x(!1),[G,K]=x(0),[q,J]=x(null),[Y,X]=x(null),Z=g(async()=>{U(!0),J(null);try{let e=await w({search:z||void 0,mediaType:H,limit:24,offset:G});e.configured===!1?(X(e.reason??`Media storage is not configured on this server.`),O([]),j(0)):(X(null),O(e.items),j(e.total))}catch(e){J(e instanceof Error&&e.message?`Failed to load media: ${e.message}`:`Failed to load media.`)}finally{U(!1)}},[w,z,H,G]);v(()=>{e&&Z()},[e,Z]);let Q=b(e),$=b(c);$.current=c,v(()=>{Q.current&&!e&&(B(``),K(0),R(new Set($.current))),Q.current=e},[e]);let oe=g(e=>{let i=L(a,e);if(i!==null){J(i);return}if(r===`single`){n([e]),t(!1);return}R(t=>{let n=new Set(t);if(n.has(e.id))n.delete(e.id);else{if(o&&n.size>=o)return t;n.add(e.id)}return n})},[r,o,a,n,t]),se=g(()=>{n(E.filter(e=>P.has(e.id))),t(!1)},[E,P,n,t]),ce=g(async e=>{if(J(null),!ee(a,e)){J(a===I?`This field takes a document or other file — “${e.name}” is an image or a video.`:`This field takes ${a} files — “${e.name}” is not one.`);return}W(!0);try{let i=await T(e),o=L(a,i);if(o!==null){J(o);return}if(r===`single`){n([i]),t(!1);return}O(e=>[i,...e]),j(e=>e+1),R(e=>new Set([...e,i.id]))}catch(e){J(e instanceof Error&&e.message?`Upload failed: ${e.message}`:`Upload failed. Please try again.`)}finally{W(!1)}},[T,r,a,n,t]);return d(p.Root,{open:e,onOpenChange:t,children:f(p.Portal,{children:[d(p.Overlay,{className:C(`fixed inset-0 z-50 bg-black/50 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0`,_?.overlay)}),f(p.Content,{...!h&&{"aria-describedby":void 0},className:C(`fixed left-1/2 top-1/2 z-50 -translate-x-1/2 -translate-y-1/2`,`flex max-h-[85vh] w-[90vw] max-w-4xl flex-col`,`rounded-xl border border-zinc-200 bg-white shadow-2xl dark:border-zinc-800 dark:bg-zinc-950`,_?.content,y),children:[f(`div`,{className:C(`flex items-center justify-between border-b border-zinc-200 px-6 py-4 dark:border-zinc-800`,_?.header),children:[d(p.Title,{className:C(`text-lg font-semibold text-zinc-900 dark:text-zinc-50`,_?.title),children:l}),h&&d(m.Root,{asChild:!0,children:d(p.Description,{children:h})}),f(p.Close,{className:`rounded-sm opacity-70 transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-zinc-400 dark:focus:ring-zinc-600`,children:[d(s,{className:`h-4 w-4`}),d(m.Root,{children:`Close`})]})]}),d(`div`,{className:C(`border-b border-zinc-200 px-6 py-3 dark:border-zinc-800`,_?.toolbar),children:d(M,{value:z,onChange:B,mediaType:V,onMediaTypeChange:ne,locked:!!a,classNames:_})}),f(`div`,{className:`flex-1 overflow-y-auto px-6 py-4`,children:[q&&d(`div`,{role:`alert`,className:`mb-4 rounded-lg border border-red-200 bg-red-50 px-4 py-3 text-sm text-red-700 dark:border-red-900 dark:bg-red-950/50 dark:text-red-400`,children:q}),Y?f(`div`,{role:`alert`,className:`rounded-lg border border-amber-200 bg-amber-50 px-4 py-6 text-center dark:border-amber-900 dark:bg-amber-950/30`,children:[d(`p`,{className:`text-sm font-medium text-amber-900 dark:text-amber-200`,children:`Media storage is not configured`}),d(`p`,{className:`mt-1 text-xs text-amber-800 dark:text-amber-300`,children:Y}),d(`p`,{className:`mt-3 text-xs text-amber-700 dark:text-amber-400`,children:`Set the required environment variables and restart the app to enable uploads.`})]}):f(u,{children:[d(N,{onUpload:ce,isUploading:ae,accept:re,classNames:_}),d(D,{items:E,selected:P,onToggle:oe,isLoading:ie,total:k,offset:G,limit:24,onPageChange:K,classNames:_})]})]}),f(`div`,{className:C(`flex items-center justify-between border-t border-zinc-200 px-6 py-4 dark:border-zinc-800`,_?.footer),children:[d(`span`,{className:`text-sm text-zinc-500 dark:text-zinc-400`,children:r===`single`?`${k.toString()} item${k===1?``:`s`} — click to select`:P.size>0?`${P.size.toString()} selected`:`${k.toString()} item${k===1?``:`s`}`}),f(`div`,{className:`flex gap-2`,children:[S,d(p.Close,{asChild:!0,children:d(`button`,{type:`button`,className:C(`rounded-md border border-zinc-300 px-4 py-2 text-sm font-medium text-zinc-700 hover:bg-zinc-50`,`dark:border-zinc-700 dark:text-zinc-300 dark:hover:bg-zinc-900`,_?.cancelButton),children:`Cancel`})}),r!==`single`&&d(`button`,{type:`button`,onClick:se,disabled:P.size===0,className:C(`rounded-md bg-blue-600 px-4 py-2 text-sm font-medium text-white hover:bg-blue-500`,`disabled:cursor-not-allowed disabled:opacity-50`,_?.confirmButton),children:`Select (${P.size.toString()})`})]})]})]})]})})}export{E as MediaCard,D as MediaGrid,z as MediaPicker,k as MediaPickerProvider,w as NON_IMAGE_MEDIA_TYPE_ICONS,M as SearchBar,N as UploadZone,S as createAdminMediaCallbacks,T as mediaTypeIcon,A as useMediaPicker};
|
|
1
|
+
"use client";import{t as e}from"./derive-media-type-C38ZA_Cl.mjs";import{Check as t,File as n,FileText as r,Film as i,Image as a,ImageIcon as o,Music as s,Search as c,Upload as l,X as u}from"lucide-react";import{clsx as d}from"clsx";import{twMerge as f}from"tailwind-merge";import{Fragment as p,jsx as m,jsxs as h}from"react/jsx-runtime";import*as g from"@radix-ui/react-dialog";import*as _ from"@radix-ui/react-visually-hidden";import{createContext as v,useCallback as y,useContext as b,useEffect as x,useMemo as S,useRef as C,useState as w}from"react";function T(e=`/api/admin`){let t=`${e}/media`,n=async e=>{try{let t=await e.clone().json();if(typeof t.error==`string`&&t.error.length>0)return t.error}catch{}try{let t=await e.text();if(t)return t}catch{}return`HTTP ${String(e.status)}`};return{fetchMedia:async e=>{let r=new URLSearchParams;e.search&&r.set(`search`,e.search),e.mediaType&&r.set(`mediaType`,e.mediaType),r.set(`limit`,String(e.limit)),r.set(`offset`,String(e.offset));let i=`${t}?${r.toString()}`,a=await fetch(i);if(!a.ok)throw Error(await n(a));return a.json()},uploadMedia:async e=>{let r=new FormData;r.append(`file`,e),r.append(`visibility`,`public`);let i=await fetch(t,{method:`POST`,body:r});if(!i.ok)throw Error(await n(i));return i.json()},getMediaUrl:async e=>{try{let n=await fetch(`${t}/${e}`);return n.ok?(await n.json()).url??null:null}catch{return null}}}}function E(...e){return f(d(e))}const D={video:i,audio:s,document:r,other:n};function O(e){return e===`image`?null:D[e]}function k(t){let n=e(t);return n===`image`?a:D[n]}function A({url:e,title:n,alt:r,mediaType:i,isSelected:a,onToggle:s,href:c,hideSelectionBadge:l,classNames:u}){let d=i===`image`&&!!e,f=r??n,g=i===void 0?null:i===`image`?o:O(i),_=E(`group relative block aspect-square overflow-hidden rounded-lg border-2 transition-all`,`focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 dark:focus:ring-offset-zinc-950`,a?`border-blue-500 ring-2 ring-blue-500/20`:`border-zinc-200 hover:border-zinc-300 dark:border-zinc-800 dark:hover:border-zinc-700`,u?.card,a&&u?.cardSelected),v=h(p,{children:[d?m(`img`,{src:e,alt:f,className:E(`h-full w-full object-cover`,u?.cardImage),loading:`lazy`}):m(`div`,{className:`flex h-full w-full items-center justify-center bg-zinc-100 dark:bg-zinc-800`,children:g&&m(g,{className:`h-8 w-8 text-zinc-400 dark:text-zinc-500`})}),a&&!l&&m(`div`,{className:`absolute right-1.5 top-1.5 flex h-5 w-5 items-center justify-center rounded-full bg-blue-500 text-white`,children:m(t,{className:`h-3 w-3`})}),m(`div`,{className:E(`absolute inset-x-0 bottom-0 bg-gradient-to-t from-black/60 to-transparent px-2 py-1.5`,`opacity-0 transition-opacity group-hover:opacity-100`,u?.cardLabel),children:m(`p`,{className:`truncate text-xs text-white`,children:n})})]});return c?m(`a`,{href:c,"aria-label":n,className:_,children:v}):m(`button`,{type:`button`,onClick:s,"aria-pressed":a,"aria-label":n,className:_,children:v})}function j({items:e,selected:t,onToggle:n,isLoading:r,total:i,offset:a,limit:o,onPageChange:s,classNames:c}){if(r)return m(`div`,{className:E(`grid grid-cols-4 gap-3 sm:grid-cols-6`,c?.grid),children:Array.from({length:12}).map((e,t)=>m(`div`,{className:E(`aspect-square animate-pulse rounded-lg bg-zinc-100 dark:bg-zinc-800`,c?.loading)},`skeleton-${t.toString()}`))});if(e.length===0)return m(`div`,{className:E(`py-12 text-center text-sm text-zinc-500 dark:text-zinc-400`,c?.empty),children:`No media found. Upload a file to get started.`});let l=Math.ceil(i/o),u=Math.floor(a/o)+1;return h(p,{children:[m(`div`,{className:E(`grid grid-cols-4 gap-3 sm:grid-cols-6`,c?.grid),children:e.map(e=>m(A,{url:e.mediaType===`image`?e.url:null,title:e.title||e.filename,alt:e.alt,mediaType:e.mediaType,isSelected:t.has(e.id),onToggle:()=>n(e),classNames:c},e.id))}),l>1&&h(`div`,{className:`mt-4 flex items-center justify-center gap-2`,children:[m(`button`,{type:`button`,disabled:u<=1,onClick:()=>s(a-o),className:`rounded-md border border-zinc-300 px-3 py-1 text-sm disabled:opacity-50 dark:border-zinc-700`,children:`Prev`}),h(`span`,{className:`text-sm text-zinc-500 dark:text-zinc-400`,children:[u,` / `,l]}),m(`button`,{type:`button`,disabled:u>=l,onClick:()=>s(a+o),className:`rounded-md border border-zinc-300 px-3 py-1 text-sm disabled:opacity-50 dark:border-zinc-700`,children:`Next`})]})]})}const M=v(null);function N({children:e,fetchMedia:t,uploadMedia:n}){return m(M,{value:S(()=>({fetchMedia:t,uploadMedia:n}),[t,n]),children:e})}function P(){let e=b(M);if(!e)throw Error(`useMediaPicker must be used within <MediaPickerProvider>. Wrap your admin layout with <MediaPickerProvider fetchMedia={...} uploadMedia={...}>.`);return e}const F=[{value:void 0,label:`All`},{value:`image`,label:`Images`},{value:`video`,label:`Videos`},{value:`audio`,label:`Audio`},{value:`document`,label:`Docs`}];function I({value:e,onChange:t,mediaType:n,onMediaTypeChange:r,locked:i,classNames:a}){let o=!i;return h(`div`,{className:`flex items-center gap-3`,children:[h(`div`,{className:`relative flex-1`,children:[m(c,{className:`absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-zinc-400`}),m(`input`,{type:`text`,value:e,onChange:e=>t(e.target.value),placeholder:`Search media...`,className:E(`w-full rounded-md border border-zinc-300 bg-transparent py-2 pl-10 pr-3 text-sm`,`placeholder:text-zinc-400 focus:border-blue-500 focus:outline-none focus:ring-1 focus:ring-blue-500`,`dark:border-zinc-700 dark:placeholder:text-zinc-500`,a?.searchInput)})]}),o&&m(`div`,{className:E(`flex gap-1`,a?.filterTabs),children:F.map(e=>m(`button`,{type:`button`,onClick:()=>r(e.value),className:E(`rounded-md px-3 py-1.5 text-xs font-medium transition-colors`,n===e.value?`bg-zinc-900 text-white dark:bg-zinc-100 dark:text-zinc-900`:`text-zinc-600 hover:bg-zinc-100 dark:text-zinc-400 dark:hover:bg-zinc-800`),children:e.label},e.label))})]})}function L({onUpload:e,isUploading:t,accept:n,classNames:r}){let i=C(null),[a,o]=w(!1),s=y(async t=>{t.preventDefault(),o(!1);let n=t.dataTransfer.files[0];n&&await e(n)},[e]),c=y(async t=>{let n=t.target.files?.[0];n&&(await e(n),t.target.value=``)},[e]);return h(`button`,{type:`button`,onDragOver:e=>{e.preventDefault(),o(!0)},onDragLeave:()=>o(!1),onDrop:s,onClick:()=>i.current?.click(),className:E(`mb-4 flex cursor-pointer flex-col items-center justify-center rounded-lg border-2 border-dashed px-6 py-6 transition-colors`,a?`border-blue-500 bg-blue-50 dark:bg-blue-950/20`:`border-zinc-300 hover:border-zinc-400 dark:border-zinc-700 dark:hover:border-zinc-600`,r?.uploadZone,a&&r?.uploadZoneActive),children:[m(l,{className:`mb-2 h-6 w-6 text-zinc-400 dark:text-zinc-500`}),m(`p`,{className:`text-sm text-zinc-600 dark:text-zinc-400`,children:t?`Uploading...`:`Drop a file here or click to upload`}),m(`input`,{ref:i,type:`file`,accept:n?.join(`,`),onChange:c,className:`hidden`})]})}const R=new Set([`image`,`video`,`audio`]);function ee(e){return e!==void 0&&R.has(e)?[`${e}/*`]:void 0}const z=`file`;function te(e,t){return e===void 0||t.type===``?!0:e===z?!t.type.startsWith(`image/`)&&!t.type.startsWith(`video/`):R.has(e)?t.type.startsWith(`${e}/`):!0}function B(e,t){if(e===void 0||t.mediaType===e)return null;let n=t.title??t.filename;return e===z?t.mediaType!==`image`&&t.mediaType!==`video`?null:`This field takes a document or other file — “${n}” is ${V(t.mediaType)}.`:`This field takes ${e} files — “${n}” is ${t.mediaType===`other`?`not one`:V(t.mediaType)}.`}function V(e){return/^[aeiou]/.test(e)?`an ${e}`:`a ${e}`}function H({open:e,onOpenChange:t,onSelect:n,mode:r=`single`,accept:i,mediaType:a,maxSelect:o,selectedIds:s=[],title:c=`Select Media`,description:l,classNames:d,className:f,children:v}){let{fetchMedia:b,uploadMedia:S}=P(),[T,D]=w([]),[O,k]=w(0),[A,M]=w(()=>new Set(s)),[N,F]=w(``),[R,V]=w(void 0),H=a??R,U=a===z?void 0:H,ne=i??ee(a),[re,W]=w(!1),[ie,G]=w(!1),[K,q]=w(0),[J,Y]=w(null),[X,Z]=w(null),Q=y(async()=>{W(!0),Y(null);try{let e=await b({search:N||void 0,mediaType:U,limit:24,offset:K});e.configured===!1?(Z(e.reason??`Media storage is not configured on this server.`),D([]),k(0)):(Z(null),D(e.items),k(e.total))}catch(e){Y(e instanceof Error&&e.message?`Failed to load media: ${e.message}`:`Failed to load media.`)}finally{W(!1)}},[b,N,U,K]);x(()=>{e&&Q()},[e,Q]);let $=C(e),ae=C(s);ae.current=s,x(()=>{$.current&&!e&&(F(``),q(0),M(new Set(ae.current))),$.current=e},[e]);let oe=y(e=>{let i=B(a,e);if(i!==null){Y(i);return}if(r===`single`){n([e]),t(!1);return}M(t=>{let n=new Set(t);if(n.has(e.id))n.delete(e.id);else{if(o&&n.size>=o)return t;n.add(e.id)}return n})},[r,o,a,n,t]),se=y(()=>{n(T.filter(e=>A.has(e.id))),t(!1)},[T,A,n,t]),ce=y(async e=>{if(Y(null),!te(a,e)){Y(a===z?`This field takes a document or other file — “${e.name}” is an image or a video.`:`This field takes ${a} files — “${e.name}” is not one.`);return}G(!0);try{let i=await S(e),o=B(a,i);if(o!==null){Y(o);return}if(r===`single`){n([i]),t(!1);return}D(e=>[i,...e]),k(e=>e+1),M(e=>new Set([...e,i.id]))}catch(e){Y(e instanceof Error&&e.message?`Upload failed: ${e.message}`:`Upload failed. Please try again.`)}finally{G(!1)}},[S,r,a,n,t]);return m(g.Root,{open:e,onOpenChange:t,children:h(g.Portal,{children:[m(g.Overlay,{className:E(`fixed inset-0 z-50 bg-black/50 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0`,d?.overlay)}),h(g.Content,{...!l&&{"aria-describedby":void 0},className:E(`fixed left-1/2 top-1/2 z-50 -translate-x-1/2 -translate-y-1/2`,`flex max-h-[85vh] w-[90vw] max-w-4xl flex-col`,`rounded-xl border border-zinc-200 bg-white shadow-2xl dark:border-zinc-800 dark:bg-zinc-950`,d?.content,f),children:[h(`div`,{className:E(`flex items-center justify-between border-b border-zinc-200 px-6 py-4 dark:border-zinc-800`,d?.header),children:[m(g.Title,{className:E(`text-lg font-semibold text-zinc-900 dark:text-zinc-50`,d?.title),children:c}),l&&m(_.Root,{asChild:!0,children:m(g.Description,{children:l})}),h(g.Close,{className:`rounded-sm opacity-70 transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-zinc-400 dark:focus:ring-zinc-600`,children:[m(u,{className:`h-4 w-4`}),m(_.Root,{children:`Close`})]})]}),m(`div`,{className:E(`border-b border-zinc-200 px-6 py-3 dark:border-zinc-800`,d?.toolbar),children:m(I,{value:N,onChange:F,mediaType:H,onMediaTypeChange:V,locked:!!a,classNames:d})}),h(`div`,{className:`flex-1 overflow-y-auto px-6 py-4`,children:[J&&m(`div`,{role:`alert`,className:`mb-4 rounded-lg border border-red-200 bg-red-50 px-4 py-3 text-sm text-red-700 dark:border-red-900 dark:bg-red-950/50 dark:text-red-400`,children:J}),X?h(`div`,{role:`alert`,className:`rounded-lg border border-amber-200 bg-amber-50 px-4 py-6 text-center dark:border-amber-900 dark:bg-amber-950/30`,children:[m(`p`,{className:`text-sm font-medium text-amber-900 dark:text-amber-200`,children:`Media storage is not configured`}),m(`p`,{className:`mt-1 text-xs text-amber-800 dark:text-amber-300`,children:X}),m(`p`,{className:`mt-3 text-xs text-amber-700 dark:text-amber-400`,children:`Set the required environment variables and restart the app to enable uploads.`})]}):h(p,{children:[m(L,{onUpload:ce,isUploading:ie,accept:ne,classNames:d}),m(j,{items:T,selected:A,onToggle:oe,isLoading:re,total:O,offset:K,limit:24,onPageChange:q,classNames:d})]})]}),h(`div`,{className:E(`flex items-center justify-between border-t border-zinc-200 px-6 py-4 dark:border-zinc-800`,d?.footer),children:[m(`span`,{className:`text-sm text-zinc-500 dark:text-zinc-400`,children:r===`single`?`${O.toString()} item${O===1?``:`s`} — click to select`:A.size>0?`${A.size.toString()} selected`:`${O.toString()} item${O===1?``:`s`}`}),h(`div`,{className:`flex gap-2`,children:[v,m(g.Close,{asChild:!0,children:m(`button`,{type:`button`,className:E(`rounded-md border border-zinc-300 px-4 py-2 text-sm font-medium text-zinc-700 hover:bg-zinc-50`,`dark:border-zinc-700 dark:text-zinc-300 dark:hover:bg-zinc-900`,d?.cancelButton),children:`Cancel`})}),r!==`single`&&m(`button`,{type:`button`,onClick:se,disabled:A.size===0,className:E(`rounded-md bg-blue-600 px-4 py-2 text-sm font-medium text-white hover:bg-blue-500`,`disabled:cursor-not-allowed disabled:opacity-50`,d?.confirmButton),children:`Select (${A.size.toString()})`})]})]})]})]})})}export{A as MediaCard,j as MediaGrid,H as MediaPicker,N as MediaPickerProvider,D as NON_IMAGE_MEDIA_TYPE_ICONS,I as SearchBar,L as UploadZone,T as createAdminMediaCallbacks,O as mediaTypeIcon,k as mimeTypeIcon,P as useMediaPicker};
|
|
3
2
|
//# sourceMappingURL=picker.mjs.map
|
package/dist/picker.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"picker.mjs","names":[],"sources":["../src/picker/admin-callbacks.ts","../src/lib/cn.ts","../src/picker/media-type-icon.ts","../src/picker/media-card.tsx","../src/picker/media-grid.tsx","../src/picker/provider.tsx","../src/picker/search-bar.tsx","../src/picker/upload-zone.tsx","../src/picker/media-picker.tsx"],"sourcesContent":["/**\n * Pre-built media callbacks that talk to the admin API.\n *\n * Eliminates boilerplate in every project — just call:\n * const media = createAdminMediaCallbacks('/api/admin')\n *\n * Returns callbacks compatible with both MediaPickerProvider and\n * BlockEditor's `media` prop (with getMediaUrl for thumbnail previews).\n */\n\nimport type { MediaPickerCallbacks, MediaPickerItem, MediaPickerListResult } from './types'\n\n/** Extended callbacks with getMediaUrl (for editor thumbnail previews) */\nexport type AdminMediaCallbacks = MediaPickerCallbacks & {\n getMediaUrl: (id: string) => Promise<string | null>\n}\n\n/**\n * Create media callbacks that talk to the admin API.\n *\n * @param apiBasePath - Base path for admin API (default: '/api/admin')\n * @returns Callbacks for MediaPickerProvider + BlockEditor media prop\n *\n * @example\n * ```tsx\n * import { createAdminMediaCallbacks } from '@murumets-ee/media/picker'\n *\n * const media = createAdminMediaCallbacks()\n * // Use with BlockEditor:\n * <BlockEditor media={media} ... />\n * // Use with MediaPickerProvider:\n * <MediaPickerProvider fetchMedia={media.fetchMedia} uploadMedia={media.uploadMedia}>\n * ```\n */\nexport function createAdminMediaCallbacks(apiBasePath = '/api/admin'): AdminMediaCallbacks {\n const baseUrl = `${apiBasePath}/media`\n\n // Admin API error responses are `{ error: string }`. Parse the body so\n // the real cause (e.g. \"Missing required environment variable:\n // STORAGE_ENDPOINT\") reaches the UI instead of just the status code.\n const readErrorMessage = async (res: Response): Promise<string> => {\n try {\n const body = (await res.clone().json()) as { error?: unknown }\n if (typeof body.error === 'string' && body.error.length > 0) return body.error\n } catch {\n // fall through — response wasn't JSON\n }\n try {\n const text = await res.text()\n if (text) return text\n } catch {\n // ignore\n }\n return `HTTP ${String(res.status)}`\n }\n\n return {\n fetchMedia: async (options: {\n search?: string | undefined\n mediaType?: string | undefined\n limit: number\n offset: number\n }): Promise<MediaPickerListResult> => {\n const params = new URLSearchParams()\n if (options.search) params.set('search', options.search)\n if (options.mediaType) params.set('mediaType', options.mediaType)\n params.set('limit', String(options.limit))\n params.set('offset', String(options.offset))\n\n const url = `${baseUrl}?${params.toString()}`\n const res = await fetch(url)\n if (!res.ok) throw new Error(await readErrorMessage(res))\n return res.json() as Promise<MediaPickerListResult>\n },\n\n uploadMedia: async (file: File): Promise<MediaPickerItem> => {\n const formData = new FormData()\n formData.append('file', file)\n // The media LIBRARY is publishable web assets, so it opts in explicitly.\n // Without this the upload route's deny-by-default leaves the asset\n // private, `getPublicUrls` returns nothing for it, and every image on the\n // public site resolves to `null` behind a 200 and one warn log (F030).\n // Sent per-upload rather than by flipping a global default, because the\n // same route also receives rent's inspection photos, which must not be\n // world-readable.\n formData.append('visibility', 'public')\n\n const res = await fetch(baseUrl, { method: 'POST', body: formData })\n if (!res.ok) throw new Error(await readErrorMessage(res))\n return res.json() as Promise<MediaPickerItem>\n },\n\n getMediaUrl: async (id: string): Promise<string | null> => {\n try {\n const res = await fetch(`${baseUrl}/${id}`)\n if (!res.ok) return null\n const item = (await res.json()) as MediaPickerItem\n return item.url ?? null\n } catch {\n return null\n }\n },\n }\n}\n","import { type ClassValue, clsx } from 'clsx'\nimport { twMerge } from 'tailwind-merge'\n\nexport function cn(...inputs: ClassValue[]) {\n return twMerge(clsx(inputs))\n}\n","import { File, FileText, Film, type LucideIcon, Music } from 'lucide-react'\n\n/**\n * The non-image members of `@murumets-ee/media`'s `MediaType`, copied rather\n * than imported.\n *\n * This module ships from the `./picker` tsdown entry — a build SEPARATE from\n * the root `index.ts` entry that already exports `MediaType` by name. Naming\n * it here too would duplicate its declaration across two `.d.mts` files\n * (`verify:dts`, #767): each array entry in `tsdown.config.ts` builds\n * independently and cannot share a declaration chunk, so a type named in two\n * entries' public APIs is emitted twice, and TypeScript can no longer treat\n * the two copies as identical if either ever gains an invariant member.\n *\n * Kept honest by `media-type-icon.test.ts`, which imports the real\n * `MediaType` (a type-only import, erased before it ever reaches a `.d.mts`)\n * and asserts this literal still matches it exactly.\n */\ntype NonImageMediaType = 'video' | 'audio' | 'document' | 'other'\n\n/**\n * The typed-icon fallback for every media type that is not `'image'`.\n *\n * The ONE rule three admin surfaces branch on (F115): the media list, the\n * picker's `MediaCard` and the media edit form all show a real `<img>` for\n * `mediaType === 'image'` and one of these icons otherwise. Extracted from\n * `MediaCard`, which held the only copy, so a fourth surface reaches for this\n * instead of writing a fourth `Record<MediaType, LucideIcon>`.\n */\nexport const NON_IMAGE_MEDIA_TYPE_ICONS: Record<NonImageMediaType, LucideIcon> = {\n video: Film,\n audio: Music,\n document: FileText,\n other: File,\n}\n\n/** The icon for a non-image media type, or `null` for `'image'` (render the asset itself). */\nexport function mediaTypeIcon(mediaType: 'image' | NonImageMediaType): LucideIcon | null {\n return mediaType === 'image' ? null : NON_IMAGE_MEDIA_TYPE_ICONS[mediaType]\n}\n","import { Check } from 'lucide-react'\nimport { cn } from '../lib/cn'\nimport { mediaTypeIcon } from './media-type-icon'\nimport type { MediaPickerClassNames, MediaPickerItem } from './types'\n\ninterface MediaCardProps {\n item: MediaPickerItem\n isSelected: boolean\n onToggle: () => void\n classNames?: MediaPickerClassNames | undefined\n}\n\nexport function MediaCard({ item, isSelected, onToggle, classNames }: MediaCardProps) {\n const isImage = item.mediaType === 'image'\n const Icon = mediaTypeIcon(item.mediaType)\n\n return (\n <button\n type=\"button\"\n onClick={onToggle}\n className={cn(\n 'group relative aspect-square overflow-hidden rounded-lg border-2 transition-all',\n 'focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 dark:focus:ring-offset-zinc-950',\n isSelected\n ? 'border-blue-500 ring-2 ring-blue-500/20'\n : 'border-zinc-200 hover:border-zinc-300 dark:border-zinc-800 dark:hover:border-zinc-700',\n classNames?.card,\n isSelected && classNames?.cardSelected,\n )}\n >\n {/* Thumbnail */}\n {isImage ? (\n <img\n src={item.url}\n alt={item.alt ?? item.filename}\n className={cn('h-full w-full object-cover', classNames?.cardImage)}\n loading=\"lazy\"\n />\n ) : (\n <div className=\"flex h-full w-full items-center justify-center bg-zinc-100 dark:bg-zinc-800\">\n {Icon && <Icon className=\"h-8 w-8 text-zinc-400 dark:text-zinc-500\" />}\n </div>\n )}\n\n {/* Selection checkmark */}\n {isSelected && (\n <div className=\"absolute right-1.5 top-1.5 flex h-5 w-5 items-center justify-center rounded-full bg-blue-500 text-white\">\n <Check className=\"h-3 w-3\" />\n </div>\n )}\n\n {/* Filename on hover */}\n <div\n className={cn(\n 'absolute inset-x-0 bottom-0 bg-gradient-to-t from-black/60 to-transparent px-2 py-1.5',\n 'opacity-0 transition-opacity group-hover:opacity-100',\n classNames?.cardLabel,\n )}\n >\n <p className=\"truncate text-xs text-white\">{item.title ?? item.filename}</p>\n </div>\n </button>\n )\n}\n","import { cn } from '../lib/cn'\nimport { MediaCard } from './media-card'\nimport type { MediaPickerClassNames, MediaPickerItem } from './types'\n\ninterface MediaGridProps {\n items: MediaPickerItem[]\n selected: Set<string>\n onToggle: (item: MediaPickerItem) => void\n isLoading: boolean\n total: number\n offset: number\n limit: number\n onPageChange: (offset: number) => void\n classNames?: MediaPickerClassNames | undefined\n}\n\nexport function MediaGrid({\n items,\n selected,\n onToggle,\n isLoading,\n total,\n offset,\n limit,\n onPageChange,\n classNames,\n}: MediaGridProps) {\n if (isLoading) {\n return (\n <div className={cn('grid grid-cols-4 gap-3 sm:grid-cols-6', classNames?.grid)}>\n {Array.from({ length: 12 }).map((_, i) => (\n <div\n key={`skeleton-${i.toString()}`}\n className={cn(\n 'aspect-square animate-pulse rounded-lg bg-zinc-100 dark:bg-zinc-800',\n classNames?.loading,\n )}\n />\n ))}\n </div>\n )\n }\n\n if (items.length === 0) {\n return (\n <div\n className={cn(\n 'py-12 text-center text-sm text-zinc-500 dark:text-zinc-400',\n classNames?.empty,\n )}\n >\n No media found. Upload a file to get started.\n </div>\n )\n }\n\n const totalPages = Math.ceil(total / limit)\n const currentPage = Math.floor(offset / limit) + 1\n\n return (\n <>\n <div className={cn('grid grid-cols-4 gap-3 sm:grid-cols-6', classNames?.grid)}>\n {items.map((item) => (\n <MediaCard\n key={item.id}\n item={item}\n isSelected={selected.has(item.id)}\n onToggle={() => onToggle(item)}\n classNames={classNames}\n />\n ))}\n </div>\n {totalPages > 1 && (\n <div className=\"mt-4 flex items-center justify-center gap-2\">\n <button\n type=\"button\"\n disabled={currentPage <= 1}\n onClick={() => onPageChange(offset - limit)}\n className=\"rounded-md border border-zinc-300 px-3 py-1 text-sm disabled:opacity-50 dark:border-zinc-700\"\n >\n Prev\n </button>\n <span className=\"text-sm text-zinc-500 dark:text-zinc-400\">\n {currentPage} / {totalPages}\n </span>\n <button\n type=\"button\"\n disabled={currentPage >= totalPages}\n onClick={() => onPageChange(offset + limit)}\n className=\"rounded-md border border-zinc-300 px-3 py-1 text-sm disabled:opacity-50 dark:border-zinc-700\"\n >\n Next\n </button>\n </div>\n )}\n </>\n )\n}\n","import { createContext, type ReactNode, useContext, useMemo } from 'react'\nimport type { MediaPickerCallbacks } from './types'\n\nconst MediaPickerContext = createContext<MediaPickerCallbacks | null>(null)\n\nexport interface MediaPickerProviderProps extends MediaPickerCallbacks {\n children: ReactNode\n}\n\n/**\n * Provides media picker callbacks to all picker instances below.\n * Wrap your admin layout with this provider.\n *\n * @example\n * ```tsx\n * <MediaPickerProvider\n * fetchMedia={fetchMediaAction}\n * uploadMedia={uploadMediaAction}\n * >\n * <AdminShell>...</AdminShell>\n * </MediaPickerProvider>\n * ```\n */\nexport function MediaPickerProvider({\n children,\n fetchMedia,\n uploadMedia,\n}: MediaPickerProviderProps) {\n const value = useMemo<MediaPickerCallbacks>(\n () => ({ fetchMedia, uploadMedia }),\n [fetchMedia, uploadMedia],\n )\n\n return <MediaPickerContext value={value}>{children}</MediaPickerContext>\n}\n\nexport function useMediaPicker(): MediaPickerCallbacks {\n const ctx = useContext(MediaPickerContext)\n if (!ctx) {\n throw new Error(\n 'useMediaPicker must be used within <MediaPickerProvider>. ' +\n 'Wrap your admin layout with <MediaPickerProvider fetchMedia={...} uploadMedia={...}>.',\n )\n }\n return ctx\n}\n","import { Search } from 'lucide-react'\nimport { cn } from '../lib/cn'\nimport type { MediaPickerClassNames } from './types'\n\ninterface SearchBarProps {\n value: string\n onChange: (value: string) => void\n mediaType: string | undefined\n onMediaTypeChange: (type: string | undefined) => void\n /** When true, the media type filter is locked (no tabs shown) */\n locked?: boolean | undefined\n classNames?: MediaPickerClassNames | undefined\n}\n\nconst FILTER_OPTIONS = [\n { value: undefined, label: 'All' },\n { value: 'image', label: 'Images' },\n { value: 'video', label: 'Videos' },\n { value: 'audio', label: 'Audio' },\n { value: 'document', label: 'Docs' },\n] as const\n\nexport function SearchBar({\n value,\n onChange,\n mediaType,\n onMediaTypeChange,\n locked,\n classNames,\n}: SearchBarProps) {\n // If locked (caller pre-set the mediaType), hide filter tabs\n const showFilters = !locked\n\n return (\n <div className=\"flex items-center gap-3\">\n <div className=\"relative flex-1\">\n <Search className=\"absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-zinc-400\" />\n <input\n type=\"text\"\n value={value}\n onChange={(e) => onChange(e.target.value)}\n placeholder=\"Search media...\"\n className={cn(\n 'w-full rounded-md border border-zinc-300 bg-transparent py-2 pl-10 pr-3 text-sm',\n 'placeholder:text-zinc-400 focus:border-blue-500 focus:outline-none focus:ring-1 focus:ring-blue-500',\n 'dark:border-zinc-700 dark:placeholder:text-zinc-500',\n classNames?.searchInput,\n )}\n />\n </div>\n {showFilters && (\n <div className={cn('flex gap-1', classNames?.filterTabs)}>\n {FILTER_OPTIONS.map((opt) => (\n <button\n key={opt.label}\n type=\"button\"\n onClick={() => onMediaTypeChange(opt.value)}\n className={cn(\n 'rounded-md px-3 py-1.5 text-xs font-medium transition-colors',\n mediaType === opt.value\n ? 'bg-zinc-900 text-white dark:bg-zinc-100 dark:text-zinc-900'\n : 'text-zinc-600 hover:bg-zinc-100 dark:text-zinc-400 dark:hover:bg-zinc-800',\n )}\n >\n {opt.label}\n </button>\n ))}\n </div>\n )}\n </div>\n )\n}\n","import { Upload } from 'lucide-react'\nimport { type DragEvent, useCallback, useRef, useState } from 'react'\nimport { cn } from '../lib/cn'\nimport type { MediaPickerClassNames } from './types'\n\ninterface UploadZoneProps {\n onUpload: (file: File) => Promise<void>\n isUploading: boolean\n accept?: string[] | undefined\n classNames?: MediaPickerClassNames | undefined\n}\n\nexport function UploadZone({ onUpload, isUploading, accept, classNames }: UploadZoneProps) {\n const inputRef = useRef<HTMLInputElement>(null)\n const [isDragging, setIsDragging] = useState(false)\n\n const handleDrop = useCallback(\n async (e: DragEvent) => {\n e.preventDefault()\n setIsDragging(false)\n const file = e.dataTransfer.files[0]\n if (file) {\n await onUpload(file)\n }\n },\n [onUpload],\n )\n\n const handleFileSelect = useCallback(\n async (e: React.ChangeEvent<HTMLInputElement>) => {\n const file = e.target.files?.[0]\n if (file) {\n await onUpload(file)\n e.target.value = ''\n }\n },\n [onUpload],\n )\n\n return (\n <button\n type=\"button\"\n onDragOver={(e) => {\n e.preventDefault()\n setIsDragging(true)\n }}\n onDragLeave={() => setIsDragging(false)}\n onDrop={handleDrop}\n onClick={() => inputRef.current?.click()}\n className={cn(\n 'mb-4 flex cursor-pointer flex-col items-center justify-center rounded-lg border-2 border-dashed px-6 py-6 transition-colors',\n isDragging\n ? 'border-blue-500 bg-blue-50 dark:bg-blue-950/20'\n : 'border-zinc-300 hover:border-zinc-400 dark:border-zinc-700 dark:hover:border-zinc-600',\n classNames?.uploadZone,\n isDragging && classNames?.uploadZoneActive,\n )}\n >\n <Upload className=\"mb-2 h-6 w-6 text-zinc-400 dark:text-zinc-500\" />\n <p className=\"text-sm text-zinc-600 dark:text-zinc-400\">\n {isUploading ? 'Uploading...' : 'Drop a file here or click to upload'}\n </p>\n <input\n ref={inputRef}\n type=\"file\"\n accept={accept?.join(',')}\n onChange={handleFileSelect}\n className=\"hidden\"\n />\n </button>\n )\n}\n","import * as DialogPrimitive from '@radix-ui/react-dialog'\nimport * as VisuallyHidden from '@radix-ui/react-visually-hidden'\nimport { X } from 'lucide-react'\nimport { useCallback, useEffect, useRef, useState } from 'react'\nimport { cn } from '../lib/cn'\nimport { MediaGrid } from './media-grid'\nimport { useMediaPicker } from './provider'\nimport { SearchBar } from './search-bar'\nimport type { MediaPickerItem, MediaPickerProps } from './types'\nimport { UploadZone } from './upload-zone'\n\nconst ITEMS_PER_PAGE = 24\n\n/** Media types whose files share one MIME top-level type (`image/*`, …). */\nconst MIME_FAMILIES = new Set(['image', 'video', 'audio'])\n\n/** The file-input patterns a locked media type implies, if it implies any. */\nfunction acceptFor(mediaType: string | undefined): string[] | undefined {\n return mediaType !== undefined && MIME_FAMILIES.has(mediaType) ? [`${mediaType}/*`] : undefined\n}\n\n/**\n * `'file'` is the residual kind — NOT an image and NOT a video — matching the\n * server's `mediaTypeMatchesKind`. It is not a list classification the media\n * API knows, so it never goes out as a browse filter.\n */\nconst RESIDUAL_KIND = 'file'\n\n/**\n * Whether a file the author chose may go into a field locked to `mediaType`,\n * judged before upload from the browser's own guess. An EMPTY `file.type` (the\n * browser could not tell — a HEIC or MKV on some platforms) passes: the server\n * sniffs the real type from the content, and the post-upload check still\n * guards the pick.\n */\nfunction fileMatches(mediaType: string | undefined, file: File): boolean {\n if (mediaType === undefined || file.type === '') return true\n if (mediaType === RESIDUAL_KIND) {\n return !file.type.startsWith('image/') && !file.type.startsWith('video/')\n }\n if (!MIME_FAMILIES.has(mediaType)) return true\n return file.type.startsWith(`${mediaType}/`)\n}\n\n/**\n * Why `item` cannot go into a field locked to `mediaType`, or `null` when it\n * can. The browse list is already filtered server-side; this is the guard for\n * what the list does not cover — an upload the server classified differently.\n */\nfunction typeMismatch(mediaType: string | undefined, item: MediaPickerItem): string | null {\n if (mediaType === undefined || item.mediaType === mediaType) return null\n const name = item.title ?? item.filename\n if (mediaType === RESIDUAL_KIND) {\n if (item.mediaType !== 'image' && item.mediaType !== 'video') return null\n return `This field takes a document or other file — “${name}” is ${withArticle(item.mediaType)}.`\n }\n return `This field takes ${mediaType} files — “${name}” is ${item.mediaType === 'other' ? 'not one' : withArticle(item.mediaType)}.`\n}\n\n/** \"an image\", \"an audio file\"-style reading: `an` before a vowel sound, `a` otherwise. */\nfunction withArticle(mediaType: MediaPickerItem['mediaType']): string {\n return /^[aeiou]/.test(mediaType) ? `an ${mediaType}` : `a ${mediaType}`\n}\n\nexport function MediaPicker({\n open,\n onOpenChange,\n onSelect,\n mode = 'single',\n accept,\n mediaType,\n maxSelect,\n selectedIds = [],\n title = 'Select Media',\n description,\n classNames,\n className,\n children,\n}: MediaPickerProps) {\n const { fetchMedia, uploadMedia } = useMediaPicker()\n\n // State\n const [items, setItems] = useState<MediaPickerItem[]>([])\n const [total, setTotal] = useState(0)\n const [selected, setSelected] = useState<Set<string>>(() => new Set(selectedIds))\n const [search, setSearch] = useState('')\n // The browse filter the author picks from the tabs. A caller's `mediaType`\n // LOCKS the filter and wins over it, derived per render rather than copied\n // into state: the dialog stays mounted between opens, so a copy taken at\n // first mount (when a host's picker had no type yet) kept listing every\n // file forever — a field asking for images was offered videos, and the\n // video landed in the image field (editor F116, reopened in PR59).\n const [mediaTypeFilter, setMediaTypeFilter] = useState<string | undefined>(undefined)\n const effectiveMediaType = mediaType ?? mediaTypeFilter\n // What the list request filters by. The residual `'file'` kind has no list\n // classification, so it browses everything and refuses images/videos on pick.\n const listMediaType = mediaType === RESIDUAL_KIND ? undefined : effectiveMediaType\n // A locked type also restricts what the file input offers, unless the caller\n // passed its own patterns.\n const effectiveAccept = accept ?? acceptFor(mediaType)\n const [isLoading, setIsLoading] = useState(false)\n const [isUploading, setIsUploading] = useState(false)\n const [offset, setOffset] = useState(0)\n const [error, setError] = useState<string | null>(null)\n // When the server reports storage isn't configured, we render a\n // banner and hide the upload zone + grid instead of failing silently\n // or showing an empty state the admin can't act on.\n const [storageDisabled, setStorageDisabled] = useState<string | null>(null)\n\n // Fetch media on open / filter change\n const loadMedia = useCallback(async () => {\n setIsLoading(true)\n setError(null)\n try {\n const result = await fetchMedia({\n search: search || undefined,\n mediaType: listMediaType,\n limit: ITEMS_PER_PAGE,\n offset,\n })\n if (result.configured === false) {\n setStorageDisabled(result.reason ?? 'Media storage is not configured on this server.')\n setItems([])\n setTotal(0)\n } else {\n setStorageDisabled(null)\n setItems(result.items)\n setTotal(result.total)\n }\n } catch (err) {\n setError(\n err instanceof Error && err.message\n ? `Failed to load media: ${err.message}`\n : 'Failed to load media.',\n )\n } finally {\n setIsLoading(false)\n }\n }, [fetchMedia, search, listMediaType, offset])\n\n useEffect(() => {\n if (open) {\n loadMedia()\n }\n }, [open, loadMedia])\n\n // Reset state when dialog closes (open transitions true → false)\n const prevOpen = useRef(open)\n const selectedIdsRef = useRef(selectedIds)\n selectedIdsRef.current = selectedIds\n useEffect(() => {\n if (prevOpen.current && !open) {\n setSearch('')\n setOffset(0)\n setSelected(new Set(selectedIdsRef.current))\n }\n prevOpen.current = open\n }, [open])\n\n // Selection\n const handleToggle = useCallback(\n (item: MediaPickerItem) => {\n const mismatch = typeMismatch(mediaType, item)\n if (mismatch !== null) {\n setError(mismatch)\n return\n }\n // Single mode: auto-confirm on click — no separate \"Select\" step needed\n if (mode === 'single') {\n onSelect([item])\n onOpenChange(false)\n return\n }\n\n // Multi mode: toggle selection in the set\n setSelected((prev) => {\n const next = new Set(prev)\n if (next.has(item.id)) {\n next.delete(item.id)\n } else {\n if (maxSelect && next.size >= maxSelect) return prev\n next.add(item.id)\n }\n return next\n })\n },\n [mode, maxSelect, mediaType, onSelect, onOpenChange],\n )\n\n const handleConfirm = useCallback(() => {\n const selectedItems = items.filter((item) => selected.has(item.id))\n onSelect(selectedItems)\n onOpenChange(false)\n }, [items, selected, onSelect, onOpenChange])\n\n // Upload\n const handleUpload = useCallback(\n async (file: File) => {\n setError(null)\n // Refused before the upload: a file the field cannot hold would otherwise\n // land in the library AND be auto-selected into the field.\n if (!fileMatches(mediaType, file)) {\n setError(\n mediaType === RESIDUAL_KIND\n ? `This field takes a document or other file — “${file.name}” is an image or a video.`\n : `This field takes ${mediaType} files — “${file.name}” is not one.`,\n )\n return\n }\n setIsUploading(true)\n try {\n const uploaded = await uploadMedia(file)\n // The server classifies the stored file; trust that over the browser's\n // guess from the extension.\n const mismatch = typeMismatch(mediaType, uploaded)\n if (mismatch !== null) {\n setError(mismatch)\n return\n }\n\n // Single mode: auto-confirm the just-uploaded file\n if (mode === 'single') {\n onSelect([uploaded])\n onOpenChange(false)\n return\n }\n\n // Multi mode: add to grid and select\n setItems((prev) => [uploaded, ...prev])\n setTotal((prev) => prev + 1)\n setSelected((prev) => new Set([...prev, uploaded.id]))\n } catch (err) {\n setError(\n err instanceof Error && err.message\n ? `Upload failed: ${err.message}`\n : 'Upload failed. Please try again.',\n )\n } finally {\n setIsUploading(false)\n }\n },\n [uploadMedia, mode, mediaType, onSelect, onOpenChange],\n )\n\n return (\n <DialogPrimitive.Root open={open} onOpenChange={onOpenChange}>\n <DialogPrimitive.Portal>\n <DialogPrimitive.Overlay\n className={cn(\n 'fixed inset-0 z-50 bg-black/50 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0',\n classNames?.overlay,\n )}\n />\n <DialogPrimitive.Content\n {...(!description && { 'aria-describedby': undefined })}\n className={cn(\n 'fixed left-1/2 top-1/2 z-50 -translate-x-1/2 -translate-y-1/2',\n 'flex max-h-[85vh] w-[90vw] max-w-4xl flex-col',\n 'rounded-xl border border-zinc-200 bg-white shadow-2xl dark:border-zinc-800 dark:bg-zinc-950',\n classNames?.content,\n className,\n )}\n >\n {/* Header */}\n <div\n className={cn(\n 'flex items-center justify-between border-b border-zinc-200 px-6 py-4 dark:border-zinc-800',\n classNames?.header,\n )}\n >\n <DialogPrimitive.Title\n className={cn(\n 'text-lg font-semibold text-zinc-900 dark:text-zinc-50',\n classNames?.title,\n )}\n >\n {title}\n </DialogPrimitive.Title>\n {description && (\n <VisuallyHidden.Root asChild>\n <DialogPrimitive.Description>{description}</DialogPrimitive.Description>\n </VisuallyHidden.Root>\n )}\n <DialogPrimitive.Close className=\"rounded-sm opacity-70 transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-zinc-400 dark:focus:ring-zinc-600\">\n <X className=\"h-4 w-4\" />\n <VisuallyHidden.Root>Close</VisuallyHidden.Root>\n </DialogPrimitive.Close>\n </div>\n\n {/* Toolbar */}\n <div\n className={cn(\n 'border-b border-zinc-200 px-6 py-3 dark:border-zinc-800',\n classNames?.toolbar,\n )}\n >\n <SearchBar\n value={search}\n onChange={setSearch}\n mediaType={effectiveMediaType}\n onMediaTypeChange={setMediaTypeFilter}\n locked={!!mediaType}\n classNames={classNames}\n />\n </div>\n\n {/* Content */}\n <div className=\"flex-1 overflow-y-auto px-6 py-4\">\n {error && (\n <div\n role=\"alert\"\n className=\"mb-4 rounded-lg border border-red-200 bg-red-50 px-4 py-3 text-sm text-red-700 dark:border-red-900 dark:bg-red-950/50 dark:text-red-400\"\n >\n {error}\n </div>\n )}\n {storageDisabled ? (\n <div\n role=\"alert\"\n className=\"rounded-lg border border-amber-200 bg-amber-50 px-4 py-6 text-center dark:border-amber-900 dark:bg-amber-950/30\"\n >\n <p className=\"text-sm font-medium text-amber-900 dark:text-amber-200\">\n Media storage is not configured\n </p>\n <p className=\"mt-1 text-xs text-amber-800 dark:text-amber-300\">{storageDisabled}</p>\n <p className=\"mt-3 text-xs text-amber-700 dark:text-amber-400\">\n Set the required environment variables and restart the app to enable uploads.\n </p>\n </div>\n ) : (\n <>\n <UploadZone\n onUpload={handleUpload}\n isUploading={isUploading}\n accept={effectiveAccept}\n classNames={classNames}\n />\n <MediaGrid\n items={items}\n selected={selected}\n onToggle={handleToggle}\n isLoading={isLoading}\n total={total}\n offset={offset}\n limit={ITEMS_PER_PAGE}\n onPageChange={setOffset}\n classNames={classNames}\n />\n </>\n )}\n </div>\n\n {/* Footer — multi mode shows confirm/cancel, single mode shows item count only */}\n <div\n className={cn(\n 'flex items-center justify-between border-t border-zinc-200 px-6 py-4 dark:border-zinc-800',\n classNames?.footer,\n )}\n >\n <span className=\"text-sm text-zinc-500 dark:text-zinc-400\">\n {mode === 'single'\n ? `${total.toString()} item${total !== 1 ? 's' : ''} — click to select`\n : selected.size > 0\n ? `${selected.size.toString()} selected`\n : `${total.toString()} item${total !== 1 ? 's' : ''}`}\n </span>\n <div className=\"flex gap-2\">\n {children}\n <DialogPrimitive.Close asChild>\n <button\n type=\"button\"\n className={cn(\n 'rounded-md border border-zinc-300 px-4 py-2 text-sm font-medium text-zinc-700 hover:bg-zinc-50',\n 'dark:border-zinc-700 dark:text-zinc-300 dark:hover:bg-zinc-900',\n classNames?.cancelButton,\n )}\n >\n Cancel\n </button>\n </DialogPrimitive.Close>\n {mode !== 'single' && (\n <button\n type=\"button\"\n onClick={handleConfirm}\n disabled={selected.size === 0}\n className={cn(\n 'rounded-md bg-blue-600 px-4 py-2 text-sm font-medium text-white hover:bg-blue-500',\n 'disabled:cursor-not-allowed disabled:opacity-50',\n classNames?.confirmButton,\n )}\n >\n {`Select (${selected.size.toString()})`}\n </button>\n )}\n </div>\n </div>\n </DialogPrimitive.Content>\n </DialogPrimitive.Portal>\n </DialogPrimitive.Root>\n )\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"picker.mjs","names":["ImageIcon"],"sources":["../src/picker/admin-callbacks.ts","../src/lib/cn.ts","../src/picker/media-type-icon.ts","../src/grid/media-card.tsx","../src/grid/media-grid.tsx","../src/picker/provider.tsx","../src/picker/search-bar.tsx","../src/picker/upload-zone.tsx","../src/picker/media-picker.tsx"],"sourcesContent":["/**\n * Pre-built media callbacks that talk to the admin API.\n *\n * Eliminates boilerplate in every project — just call:\n * const media = createAdminMediaCallbacks('/api/admin')\n *\n * Returns callbacks compatible with both MediaPickerProvider and\n * BlockEditor's `media` prop (with getMediaUrl for thumbnail previews).\n */\n\nimport type { MediaPickerCallbacks, MediaPickerItem, MediaPickerListResult } from './types'\n\n/** Extended callbacks with getMediaUrl (for editor thumbnail previews) */\nexport type AdminMediaCallbacks = MediaPickerCallbacks & {\n getMediaUrl: (id: string) => Promise<string | null>\n}\n\n/**\n * Create media callbacks that talk to the admin API.\n *\n * @param apiBasePath - Base path for admin API (default: '/api/admin')\n * @returns Callbacks for MediaPickerProvider + BlockEditor media prop\n *\n * @example\n * ```tsx\n * import { createAdminMediaCallbacks } from '@murumets-ee/media/picker'\n *\n * const media = createAdminMediaCallbacks()\n * // Use with BlockEditor:\n * <BlockEditor media={media} ... />\n * // Use with MediaPickerProvider:\n * <MediaPickerProvider fetchMedia={media.fetchMedia} uploadMedia={media.uploadMedia}>\n * ```\n */\nexport function createAdminMediaCallbacks(apiBasePath = '/api/admin'): AdminMediaCallbacks {\n const baseUrl = `${apiBasePath}/media`\n\n // Admin API error responses are `{ error: string }`. Parse the body so\n // the real cause (e.g. \"Missing required environment variable:\n // STORAGE_ENDPOINT\") reaches the UI instead of just the status code.\n const readErrorMessage = async (res: Response): Promise<string> => {\n try {\n const body = (await res.clone().json()) as { error?: unknown }\n if (typeof body.error === 'string' && body.error.length > 0) return body.error\n } catch {\n // fall through — response wasn't JSON\n }\n try {\n const text = await res.text()\n if (text) return text\n } catch {\n // ignore\n }\n return `HTTP ${String(res.status)}`\n }\n\n return {\n fetchMedia: async (options: {\n search?: string | undefined\n mediaType?: string | undefined\n limit: number\n offset: number\n }): Promise<MediaPickerListResult> => {\n const params = new URLSearchParams()\n if (options.search) params.set('search', options.search)\n if (options.mediaType) params.set('mediaType', options.mediaType)\n params.set('limit', String(options.limit))\n params.set('offset', String(options.offset))\n\n const url = `${baseUrl}?${params.toString()}`\n const res = await fetch(url)\n if (!res.ok) throw new Error(await readErrorMessage(res))\n return res.json() as Promise<MediaPickerListResult>\n },\n\n uploadMedia: async (file: File): Promise<MediaPickerItem> => {\n const formData = new FormData()\n formData.append('file', file)\n // The media LIBRARY is publishable web assets, so it opts in explicitly.\n // Without this the upload route's deny-by-default leaves the asset\n // private, `getPublicUrls` returns nothing for it, and every image on the\n // public site resolves to `null` behind a 200 and one warn log (F030).\n // Sent per-upload rather than by flipping a global default, because the\n // same route also receives rent's inspection photos, which must not be\n // world-readable.\n formData.append('visibility', 'public')\n\n const res = await fetch(baseUrl, { method: 'POST', body: formData })\n if (!res.ok) throw new Error(await readErrorMessage(res))\n return res.json() as Promise<MediaPickerItem>\n },\n\n getMediaUrl: async (id: string): Promise<string | null> => {\n try {\n const res = await fetch(`${baseUrl}/${id}`)\n if (!res.ok) return null\n const item = (await res.json()) as MediaPickerItem\n return item.url ?? null\n } catch {\n return null\n }\n },\n }\n}\n","import { type ClassValue, clsx } from 'clsx'\nimport { twMerge } from 'tailwind-merge'\n\nexport function cn(...inputs: ClassValue[]) {\n return twMerge(clsx(inputs))\n}\n","import { File, FileText, Film, Image as ImageIcon, type LucideIcon, Music } from 'lucide-react'\nimport { deriveMediaType } from '../derive-media-type.js'\n\n/**\n * The non-image members of `@murumets-ee/media`'s `MediaType`, copied rather\n * than imported.\n *\n * This module ships from the `./picker` tsdown entry — a build SEPARATE from\n * the root `index.ts` entry that already exports `MediaType` by name. Naming\n * it here too would duplicate its declaration across two `.d.mts` files\n * (`verify:dts`, #767): each array entry in `tsdown.config.ts` builds\n * independently and cannot share a declaration chunk, so a type named in two\n * entries' public APIs is emitted twice, and TypeScript can no longer treat\n * the two copies as identical if either ever gains an invariant member.\n *\n * Kept honest by `media-type-icon.test.ts`, which imports the real\n * `MediaType` (a type-only import, erased before it ever reaches a `.d.mts`)\n * and asserts this literal still matches it exactly.\n */\ntype NonImageMediaType = 'video' | 'audio' | 'document' | 'other'\n\n/**\n * The typed-icon fallback for every media type that is not `'image'`.\n *\n * The ONE rule three admin surfaces branch on (F115): the media list, the\n * picker's `MediaCard` and the media edit form all show a real `<img>` for\n * `mediaType === 'image'` and one of these icons otherwise. Extracted from\n * `MediaCard`, which held the only copy, so a fourth surface reaches for this\n * instead of writing a fourth `Record<MediaType, LucideIcon>`.\n */\nexport const NON_IMAGE_MEDIA_TYPE_ICONS: Record<NonImageMediaType, LucideIcon> = {\n video: Film,\n audio: Music,\n document: FileText,\n other: File,\n}\n\n/** The icon for a non-image media type, or `null` for `'image'` (render the asset itself). */\nexport function mediaTypeIcon(mediaType: 'image' | NonImageMediaType): LucideIcon | null {\n return mediaType === 'image' ? null : NON_IMAGE_MEDIA_TYPE_ICONS[mediaType]\n}\n\n/**\n * The icon for a MIME type — the admin list's MIME column, where the string\n * `video/mp4` is replaced by a film (entity-table D008's `mimeType` display).\n *\n * Unlike {@link mediaTypeIcon}, an image gets one too: that function returns\n * `null` for `'image'` because its callers draw the ASSET instead, and a column\n * that shows a type has no asset to draw. The classification is\n * `deriveMediaType` — the rule the upload stored the row's `mediaType` by — so\n * the icon cannot disagree with the type column beside it.\n *\n * Takes a plain string and returns a `LucideIcon`, so `MediaType` is not named\n * in this entry's public API (see the note above `NonImageMediaType`).\n */\nexport function mimeTypeIcon(mimeType: string): LucideIcon {\n const mediaType = deriveMediaType(mimeType)\n return mediaType === 'image' ? ImageIcon : NON_IMAGE_MEDIA_TYPE_ICONS[mediaType]\n}\n","import { Check, ImageIcon } from 'lucide-react'\nimport { cn } from '../lib/cn'\nimport { mediaTypeIcon } from '../picker/media-type-icon'\nimport type { MediaPickerClassNames } from '../picker/types'\n\n/**\n * A structural copy of `@murumets-ee/media`'s own `MediaType`, not an\n * import — same reason `media-type-icon.ts`'s `NonImageMediaType` isn't one.\n * This module ships from the `./picker` tsdown entry (re-exported by\n * `picker/index.ts`), a build SEPARATE from the root `index.ts` entry that\n * already exports `MediaType` by name; importing (and thereby re-exporting,\n * via `MediaCardProps`) it here too duplicates its declaration across two\n * `.d.mts` files (`verify:dts`, #767) the moment this type reaches the\n * entry's own public surface.\n */\ntype MediaCardMediaType = 'image' | 'video' | 'audio' | 'document' | 'other'\n\n/**\n * One tile: a thumbnail (or a typed icon for anything that is not an image),\n * a hover title overlay, and a selection checkmark.\n *\n * Promoted out of `picker/` (entity-table PR06 §2) — this is the whole point\n * of the promotion. `<MediaPicker>`'s `MediaGrid` and the admin-ui entity\n * list's `grid` layout renderer both draw tiles through this ONE component,\n * so a feature added to one reaches both rather than diverging the way\n * WordPress's Backbone grid and `WP_List_Table` did (D007's cautionary tale).\n *\n * Decoupled from `MediaPickerItem` on purpose: the entity list's grid layout\n * has no `MediaPickerItem` to hand it — it has a TanStack row and a\n * `ViewGridOptions`-resolved field name — so the props here are the minimal\n * shape both callers can build without synthesising fake data for fields\n * neither has (a picker item's `size`/`width`/`height`, an entity row's MIME\n * type).\n */\nexport interface MediaCardProps {\n /** Resolved thumbnail/asset URL. `null` (or a non-`'image'` `mediaType`) draws the typed icon instead. */\n url: string | null\n /** The tile's accessible name and its hover-overlay label. */\n title: string\n /**\n * The image's OWN alt text, when the caller has one — an editor-authored\n * field distinct from `title` (a display name most often equal to the\n * filename). Falls back to `title` only when omitted (`undefined`) or\n * `null` — a deliberately EMPTY string (`''`, the WCAG decorative-image\n * marker) is preserved, not overridden. Review finding, entity-table PR06:\n * the promotion had collapsed this into `title` and silently dropped a\n * screen-reader-meaningful alt text down to the display name; a follow-up\n * critique pass then caught the first fix using `||` (which clobbers `''`\n * the same way) instead of `??`.\n */\n alt?: string | null | undefined\n /**\n * The item's classification. `'image'` draws `url` as an `<img>`;\n * anything else — including `undefined`, an entity with no classification\n * field at all — draws a typed (or generic) icon, never a broken `<img>`.\n */\n mediaType: MediaCardMediaType | undefined\n isSelected: boolean\n onToggle: () => void\n /**\n * Renders the tile as a real `<a href>` instead of a select button — the\n * admin-ui entity list's grid layout (entity-table D004 ported to a card:\n * the tile is the row's link, same as the table's title cell) sets this so\n * cmd/ctrl-click, middle-click and \"open in new tab\" all work, exactly the\n * capabilities S4 requires of the table. `onToggle` and `isSelected` still\n * apply — the caller's OWN checkbox overlay (drawn outside this component,\n * at a higher z-index) is what fires `onToggle`, not a click on the tile.\n *\n * Omitted (the picker's own use, `<MediaGrid>`) keeps the whole tile a\n * select button, unchanged from before the promotion.\n */\n href?: string | undefined\n /**\n * Suppresses this tile's own checkmark badge — driven by whether the\n * CALLER is drawing a DIFFERENT overlay in the same top-right corner, not\n * by `href`. Critique finding, entity-table PR06: the first cut of the\n * overlay-collision fix keyed this off `!!href`, which only happens to\n * equal \"does the caller draw an actions overlay here\" for today's one\n * caller (Media, where `editHref` and `allowDelete` are both always on).\n * The moment an entity reaches the grid through the generic list page with\n * a permission-gated `editHref` (so `href` can be `undefined`) while still\n * allowing delete (so an actions overlay IS drawn), `!!href` goes false\n * and the badge reappears under the menu it was meant to stay clear of.\n * `EntityGridLayout` passes the caller's own truth — whether it rendered\n * an `_actions` cell for this row — instead of re-deriving it from `href`.\n */\n hideSelectionBadge?: boolean | undefined\n classNames?: MediaPickerClassNames | undefined\n}\n\nexport function MediaCard({\n url,\n title,\n alt,\n mediaType,\n isSelected,\n onToggle,\n href,\n hideSelectionBadge,\n classNames,\n}: MediaCardProps) {\n const isImage = mediaType === 'image' && !!url\n // `??`, not `||`: an editor-set EMPTY alt (`''`) is the WCAG-legitimate way\n // to mark an image decorative — a screen reader should skip it, not get\n // `title` instead. `||` would treat that deliberate `''` the same as\n // \"never set\" and silently override it. Matches what the pre-promotion\n // `<img alt={item.alt ?? item.filename}>` already did.\n const imgAlt = alt ?? title\n // `mediaTypeIcon('image')` is `null` on purpose — its OTHER caller\n // (`ImageCell`) draws the asset for that branch, so it never asks for an\n // icon. Here that branch is exactly the row this tile has no asset FOR\n // (an image row with no processed thumbnail — a real, reachable state,\n // not a hypothetical: found via browser verification, entity-table PR06)\n // — matching `ImageCell`'s own \"no url yet\" fallback rather than the\n // BLANK tile `mediaTypeIcon`'s `null` would otherwise draw.\n const Icon =\n mediaType === undefined ? null : mediaType === 'image' ? ImageIcon : mediaTypeIcon(mediaType)\n\n const sharedClassName = cn(\n // `block`: a `<button>` defaults to `inline-block` (width/height already\n // apply), but a plain `<a>` defaults to `inline` — CSS gives width/height\n // NO EFFECT on a non-replaced inline element, so the `w-full`\n // (`classNames.card`, set by callers) and `aspect-square` below did\n // nothing for the href-mode tile: it collapsed to its content's\n // intrinsic size instead of filling the grid track (CodeRabbit CLI\n // finding, entity-table PR06). Harmless on `<button>`, which already\n // behaves this way.\n 'group relative block aspect-square overflow-hidden rounded-lg border-2 transition-all',\n 'focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 dark:focus:ring-offset-zinc-950',\n isSelected\n ? 'border-blue-500 ring-2 ring-blue-500/20'\n : 'border-zinc-200 hover:border-zinc-300 dark:border-zinc-800 dark:hover:border-zinc-700',\n classNames?.card,\n isSelected && classNames?.cardSelected,\n )\n\n const content = (\n <>\n {/* Thumbnail */}\n {isImage ? (\n // biome-ignore lint/performance/noImgElement: same reasoning as admin-ui's ImageCell — this ships in a published toolkit package consumed by Next AND non-Next harnesses.\n <img\n src={url}\n alt={imgAlt}\n className={cn('h-full w-full object-cover', classNames?.cardImage)}\n loading=\"lazy\"\n />\n ) : (\n <div className=\"flex h-full w-full items-center justify-center bg-zinc-100 dark:bg-zinc-800\">\n {Icon && <Icon className=\"h-8 w-8 text-zinc-400 dark:text-zinc-500\" />}\n </div>\n )}\n\n {/*\n Selection checkmark, suppressed only when the CALLER says it is\n drawing something else in this corner (`hideSelectionBadge`) — see\n that prop's doc for why this is no longer inferred from `href`. The\n entity list's grid layout draws its own checkbox overlay at the\n top-LEFT and, when the row has one, a row-actions menu at the\n top-RIGHT — the same corner this badge used to claim unconditionally,\n so a selected AND hovered tile stacked the \"⋯\" button directly on top\n of it. The border/ring highlight above is still selection's primary\n signal whenever the badge is hidden; the picker (button mode, never\n passes this prop) keeps the badge as its only one.\n */}\n {isSelected && !hideSelectionBadge && (\n <div className=\"absolute right-1.5 top-1.5 flex h-5 w-5 items-center justify-center rounded-full bg-blue-500 text-white\">\n <Check className=\"h-3 w-3\" />\n </div>\n )}\n\n {/* Title on hover */}\n <div\n className={cn(\n 'absolute inset-x-0 bottom-0 bg-gradient-to-t from-black/60 to-transparent px-2 py-1.5',\n 'opacity-0 transition-opacity group-hover:opacity-100',\n classNames?.cardLabel,\n )}\n >\n <p className=\"truncate text-xs text-white\">{title}</p>\n </div>\n </>\n )\n\n // Two literal branches rather than a polymorphic `Root = href ? 'a' : 'button'`\n // tag: an anchor and a button take different, non-overlapping prop sets\n // (`href` vs `type`/`onClick`), and a runtime-chosen tag name can't be typed\n // against both without `any`.\n if (href) {\n return (\n <a href={href} aria-label={title} className={sharedClassName}>\n {content}\n </a>\n )\n }\n return (\n <button\n type=\"button\"\n onClick={onToggle}\n aria-pressed={isSelected}\n aria-label={title}\n className={sharedClassName}\n >\n {content}\n </button>\n )\n}\n","import { cn } from '../lib/cn'\nimport type { MediaPickerClassNames, MediaPickerItem } from '../picker/types'\nimport { MediaCard } from './media-card'\n\ninterface MediaGridProps {\n items: MediaPickerItem[]\n selected: Set<string>\n onToggle: (item: MediaPickerItem) => void\n isLoading: boolean\n total: number\n offset: number\n limit: number\n onPageChange: (offset: number) => void\n classNames?: MediaPickerClassNames | undefined\n}\n\n/**\n * The `<MediaPicker>` dialog's own grid — its pagination (offset/limit, a\n * Prev/Next footer) is the picker's, not the entity list's. The TILE it\n * draws is the promoted, shared `MediaCard` (entity-table PR06 §2); this\n * wrapper's job is adapting `MediaPickerItem[]` to that tile's generic props.\n */\nexport function MediaGrid({\n items,\n selected,\n onToggle,\n isLoading,\n total,\n offset,\n limit,\n onPageChange,\n classNames,\n}: MediaGridProps) {\n if (isLoading) {\n return (\n <div className={cn('grid grid-cols-4 gap-3 sm:grid-cols-6', classNames?.grid)}>\n {Array.from({ length: 12 }).map((_, i) => (\n <div\n key={`skeleton-${i.toString()}`}\n className={cn(\n 'aspect-square animate-pulse rounded-lg bg-zinc-100 dark:bg-zinc-800',\n classNames?.loading,\n )}\n />\n ))}\n </div>\n )\n }\n\n if (items.length === 0) {\n return (\n <div\n className={cn(\n 'py-12 text-center text-sm text-zinc-500 dark:text-zinc-400',\n classNames?.empty,\n )}\n >\n No media found. Upload a file to get started.\n </div>\n )\n }\n\n const totalPages = Math.ceil(total / limit)\n const currentPage = Math.floor(offset / limit) + 1\n\n return (\n <>\n <div className={cn('grid grid-cols-4 gap-3 sm:grid-cols-6', classNames?.grid)}>\n {items.map((item) => (\n <MediaCard\n key={item.id}\n url={item.mediaType === 'image' ? item.url : null}\n // `||`, not `??`: unlike `alt` (where an editor-set `''` is the\n // WCAG-legitimate \"decorative, skip me\" marker worth preserving\n // — see `media-card.tsx`), an empty TITLE has no such meaning.\n // `??` alone left it through for an item with `title: ''`\n // (distinct from `title: null`), and `title` is what MediaCard's\n // `aria-label` uses directly — an empty accessible name, not just\n // a missing caption (CodeRabbit CLI finding, entity-table PR06).\n title={item.title || item.filename}\n alt={item.alt}\n mediaType={item.mediaType}\n isSelected={selected.has(item.id)}\n onToggle={() => onToggle(item)}\n classNames={classNames}\n />\n ))}\n </div>\n {totalPages > 1 && (\n <div className=\"mt-4 flex items-center justify-center gap-2\">\n <button\n type=\"button\"\n disabled={currentPage <= 1}\n onClick={() => onPageChange(offset - limit)}\n className=\"rounded-md border border-zinc-300 px-3 py-1 text-sm disabled:opacity-50 dark:border-zinc-700\"\n >\n Prev\n </button>\n <span className=\"text-sm text-zinc-500 dark:text-zinc-400\">\n {currentPage} / {totalPages}\n </span>\n <button\n type=\"button\"\n disabled={currentPage >= totalPages}\n onClick={() => onPageChange(offset + limit)}\n className=\"rounded-md border border-zinc-300 px-3 py-1 text-sm disabled:opacity-50 dark:border-zinc-700\"\n >\n Next\n </button>\n </div>\n )}\n </>\n )\n}\n","import { createContext, type ReactNode, useContext, useMemo } from 'react'\nimport type { MediaPickerCallbacks } from './types'\n\nconst MediaPickerContext = createContext<MediaPickerCallbacks | null>(null)\n\nexport interface MediaPickerProviderProps extends MediaPickerCallbacks {\n children: ReactNode\n}\n\n/**\n * Provides media picker callbacks to all picker instances below.\n * Wrap your admin layout with this provider.\n *\n * @example\n * ```tsx\n * <MediaPickerProvider\n * fetchMedia={fetchMediaAction}\n * uploadMedia={uploadMediaAction}\n * >\n * <AdminShell>...</AdminShell>\n * </MediaPickerProvider>\n * ```\n */\nexport function MediaPickerProvider({\n children,\n fetchMedia,\n uploadMedia,\n}: MediaPickerProviderProps) {\n const value = useMemo<MediaPickerCallbacks>(\n () => ({ fetchMedia, uploadMedia }),\n [fetchMedia, uploadMedia],\n )\n\n return <MediaPickerContext value={value}>{children}</MediaPickerContext>\n}\n\nexport function useMediaPicker(): MediaPickerCallbacks {\n const ctx = useContext(MediaPickerContext)\n if (!ctx) {\n throw new Error(\n 'useMediaPicker must be used within <MediaPickerProvider>. ' +\n 'Wrap your admin layout with <MediaPickerProvider fetchMedia={...} uploadMedia={...}>.',\n )\n }\n return ctx\n}\n","import { Search } from 'lucide-react'\nimport { cn } from '../lib/cn'\nimport type { MediaPickerClassNames } from './types'\n\ninterface SearchBarProps {\n value: string\n onChange: (value: string) => void\n mediaType: string | undefined\n onMediaTypeChange: (type: string | undefined) => void\n /** When true, the media type filter is locked (no tabs shown) */\n locked?: boolean | undefined\n classNames?: MediaPickerClassNames | undefined\n}\n\nconst FILTER_OPTIONS = [\n { value: undefined, label: 'All' },\n { value: 'image', label: 'Images' },\n { value: 'video', label: 'Videos' },\n { value: 'audio', label: 'Audio' },\n { value: 'document', label: 'Docs' },\n] as const\n\nexport function SearchBar({\n value,\n onChange,\n mediaType,\n onMediaTypeChange,\n locked,\n classNames,\n}: SearchBarProps) {\n // If locked (caller pre-set the mediaType), hide filter tabs\n const showFilters = !locked\n\n return (\n <div className=\"flex items-center gap-3\">\n <div className=\"relative flex-1\">\n <Search className=\"absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-zinc-400\" />\n <input\n type=\"text\"\n value={value}\n onChange={(e) => onChange(e.target.value)}\n placeholder=\"Search media...\"\n className={cn(\n 'w-full rounded-md border border-zinc-300 bg-transparent py-2 pl-10 pr-3 text-sm',\n 'placeholder:text-zinc-400 focus:border-blue-500 focus:outline-none focus:ring-1 focus:ring-blue-500',\n 'dark:border-zinc-700 dark:placeholder:text-zinc-500',\n classNames?.searchInput,\n )}\n />\n </div>\n {showFilters && (\n <div className={cn('flex gap-1', classNames?.filterTabs)}>\n {FILTER_OPTIONS.map((opt) => (\n <button\n key={opt.label}\n type=\"button\"\n onClick={() => onMediaTypeChange(opt.value)}\n className={cn(\n 'rounded-md px-3 py-1.5 text-xs font-medium transition-colors',\n mediaType === opt.value\n ? 'bg-zinc-900 text-white dark:bg-zinc-100 dark:text-zinc-900'\n : 'text-zinc-600 hover:bg-zinc-100 dark:text-zinc-400 dark:hover:bg-zinc-800',\n )}\n >\n {opt.label}\n </button>\n ))}\n </div>\n )}\n </div>\n )\n}\n","import { Upload } from 'lucide-react'\nimport { type DragEvent, useCallback, useRef, useState } from 'react'\nimport { cn } from '../lib/cn'\nimport type { MediaPickerClassNames } from './types'\n\ninterface UploadZoneProps {\n onUpload: (file: File) => Promise<void>\n isUploading: boolean\n accept?: string[] | undefined\n classNames?: MediaPickerClassNames | undefined\n}\n\nexport function UploadZone({ onUpload, isUploading, accept, classNames }: UploadZoneProps) {\n const inputRef = useRef<HTMLInputElement>(null)\n const [isDragging, setIsDragging] = useState(false)\n\n const handleDrop = useCallback(\n async (e: DragEvent) => {\n e.preventDefault()\n setIsDragging(false)\n const file = e.dataTransfer.files[0]\n if (file) {\n await onUpload(file)\n }\n },\n [onUpload],\n )\n\n const handleFileSelect = useCallback(\n async (e: React.ChangeEvent<HTMLInputElement>) => {\n const file = e.target.files?.[0]\n if (file) {\n await onUpload(file)\n e.target.value = ''\n }\n },\n [onUpload],\n )\n\n return (\n <button\n type=\"button\"\n onDragOver={(e) => {\n e.preventDefault()\n setIsDragging(true)\n }}\n onDragLeave={() => setIsDragging(false)}\n onDrop={handleDrop}\n onClick={() => inputRef.current?.click()}\n className={cn(\n 'mb-4 flex cursor-pointer flex-col items-center justify-center rounded-lg border-2 border-dashed px-6 py-6 transition-colors',\n isDragging\n ? 'border-blue-500 bg-blue-50 dark:bg-blue-950/20'\n : 'border-zinc-300 hover:border-zinc-400 dark:border-zinc-700 dark:hover:border-zinc-600',\n classNames?.uploadZone,\n isDragging && classNames?.uploadZoneActive,\n )}\n >\n <Upload className=\"mb-2 h-6 w-6 text-zinc-400 dark:text-zinc-500\" />\n <p className=\"text-sm text-zinc-600 dark:text-zinc-400\">\n {isUploading ? 'Uploading...' : 'Drop a file here or click to upload'}\n </p>\n <input\n ref={inputRef}\n type=\"file\"\n accept={accept?.join(',')}\n onChange={handleFileSelect}\n className=\"hidden\"\n />\n </button>\n )\n}\n","import * as DialogPrimitive from '@radix-ui/react-dialog'\nimport * as VisuallyHidden from '@radix-ui/react-visually-hidden'\nimport { X } from 'lucide-react'\nimport { useCallback, useEffect, useRef, useState } from 'react'\nimport { cn } from '../lib/cn'\nimport { MediaGrid } from '../grid/media-grid'\nimport { useMediaPicker } from './provider'\nimport { SearchBar } from './search-bar'\nimport type { MediaPickerItem, MediaPickerProps } from './types'\nimport { UploadZone } from './upload-zone'\n\nconst ITEMS_PER_PAGE = 24\n\n/** Media types whose files share one MIME top-level type (`image/*`, …). */\nconst MIME_FAMILIES = new Set(['image', 'video', 'audio'])\n\n/** The file-input patterns a locked media type implies, if it implies any. */\nfunction acceptFor(mediaType: string | undefined): string[] | undefined {\n return mediaType !== undefined && MIME_FAMILIES.has(mediaType) ? [`${mediaType}/*`] : undefined\n}\n\n/**\n * `'file'` is the residual kind — NOT an image and NOT a video — matching the\n * server's `mediaTypeMatchesKind`. It is not a list classification the media\n * API knows, so it never goes out as a browse filter.\n */\nconst RESIDUAL_KIND = 'file'\n\n/**\n * Whether a file the author chose may go into a field locked to `mediaType`,\n * judged before upload from the browser's own guess. An EMPTY `file.type` (the\n * browser could not tell — a HEIC or MKV on some platforms) passes: the server\n * sniffs the real type from the content, and the post-upload check still\n * guards the pick.\n */\nfunction fileMatches(mediaType: string | undefined, file: File): boolean {\n if (mediaType === undefined || file.type === '') return true\n if (mediaType === RESIDUAL_KIND) {\n return !file.type.startsWith('image/') && !file.type.startsWith('video/')\n }\n if (!MIME_FAMILIES.has(mediaType)) return true\n return file.type.startsWith(`${mediaType}/`)\n}\n\n/**\n * Why `item` cannot go into a field locked to `mediaType`, or `null` when it\n * can. The browse list is already filtered server-side; this is the guard for\n * what the list does not cover — an upload the server classified differently.\n */\nfunction typeMismatch(mediaType: string | undefined, item: MediaPickerItem): string | null {\n if (mediaType === undefined || item.mediaType === mediaType) return null\n const name = item.title ?? item.filename\n if (mediaType === RESIDUAL_KIND) {\n if (item.mediaType !== 'image' && item.mediaType !== 'video') return null\n return `This field takes a document or other file — “${name}” is ${withArticle(item.mediaType)}.`\n }\n return `This field takes ${mediaType} files — “${name}” is ${item.mediaType === 'other' ? 'not one' : withArticle(item.mediaType)}.`\n}\n\n/** \"an image\", \"an audio file\"-style reading: `an` before a vowel sound, `a` otherwise. */\nfunction withArticle(mediaType: MediaPickerItem['mediaType']): string {\n return /^[aeiou]/.test(mediaType) ? `an ${mediaType}` : `a ${mediaType}`\n}\n\nexport function MediaPicker({\n open,\n onOpenChange,\n onSelect,\n mode = 'single',\n accept,\n mediaType,\n maxSelect,\n selectedIds = [],\n title = 'Select Media',\n description,\n classNames,\n className,\n children,\n}: MediaPickerProps) {\n const { fetchMedia, uploadMedia } = useMediaPicker()\n\n // State\n const [items, setItems] = useState<MediaPickerItem[]>([])\n const [total, setTotal] = useState(0)\n const [selected, setSelected] = useState<Set<string>>(() => new Set(selectedIds))\n const [search, setSearch] = useState('')\n // The browse filter the author picks from the tabs. A caller's `mediaType`\n // LOCKS the filter and wins over it, derived per render rather than copied\n // into state: the dialog stays mounted between opens, so a copy taken at\n // first mount (when a host's picker had no type yet) kept listing every\n // file forever — a field asking for images was offered videos, and the\n // video landed in the image field (editor F116, reopened in PR59).\n const [mediaTypeFilter, setMediaTypeFilter] = useState<string | undefined>(undefined)\n const effectiveMediaType = mediaType ?? mediaTypeFilter\n // What the list request filters by. The residual `'file'` kind has no list\n // classification, so it browses everything and refuses images/videos on pick.\n const listMediaType = mediaType === RESIDUAL_KIND ? undefined : effectiveMediaType\n // A locked type also restricts what the file input offers, unless the caller\n // passed its own patterns.\n const effectiveAccept = accept ?? acceptFor(mediaType)\n const [isLoading, setIsLoading] = useState(false)\n const [isUploading, setIsUploading] = useState(false)\n const [offset, setOffset] = useState(0)\n const [error, setError] = useState<string | null>(null)\n // When the server reports storage isn't configured, we render a\n // banner and hide the upload zone + grid instead of failing silently\n // or showing an empty state the admin can't act on.\n const [storageDisabled, setStorageDisabled] = useState<string | null>(null)\n\n // Fetch media on open / filter change\n const loadMedia = useCallback(async () => {\n setIsLoading(true)\n setError(null)\n try {\n const result = await fetchMedia({\n search: search || undefined,\n mediaType: listMediaType,\n limit: ITEMS_PER_PAGE,\n offset,\n })\n if (result.configured === false) {\n setStorageDisabled(result.reason ?? 'Media storage is not configured on this server.')\n setItems([])\n setTotal(0)\n } else {\n setStorageDisabled(null)\n setItems(result.items)\n setTotal(result.total)\n }\n } catch (err) {\n setError(\n err instanceof Error && err.message\n ? `Failed to load media: ${err.message}`\n : 'Failed to load media.',\n )\n } finally {\n setIsLoading(false)\n }\n }, [fetchMedia, search, listMediaType, offset])\n\n useEffect(() => {\n if (open) {\n loadMedia()\n }\n }, [open, loadMedia])\n\n // Reset state when dialog closes (open transitions true → false)\n const prevOpen = useRef(open)\n const selectedIdsRef = useRef(selectedIds)\n selectedIdsRef.current = selectedIds\n useEffect(() => {\n if (prevOpen.current && !open) {\n setSearch('')\n setOffset(0)\n setSelected(new Set(selectedIdsRef.current))\n }\n prevOpen.current = open\n }, [open])\n\n // Selection\n const handleToggle = useCallback(\n (item: MediaPickerItem) => {\n const mismatch = typeMismatch(mediaType, item)\n if (mismatch !== null) {\n setError(mismatch)\n return\n }\n // Single mode: auto-confirm on click — no separate \"Select\" step needed\n if (mode === 'single') {\n onSelect([item])\n onOpenChange(false)\n return\n }\n\n // Multi mode: toggle selection in the set\n setSelected((prev) => {\n const next = new Set(prev)\n if (next.has(item.id)) {\n next.delete(item.id)\n } else {\n if (maxSelect && next.size >= maxSelect) return prev\n next.add(item.id)\n }\n return next\n })\n },\n [mode, maxSelect, mediaType, onSelect, onOpenChange],\n )\n\n const handleConfirm = useCallback(() => {\n const selectedItems = items.filter((item) => selected.has(item.id))\n onSelect(selectedItems)\n onOpenChange(false)\n }, [items, selected, onSelect, onOpenChange])\n\n // Upload\n const handleUpload = useCallback(\n async (file: File) => {\n setError(null)\n // Refused before the upload: a file the field cannot hold would otherwise\n // land in the library AND be auto-selected into the field.\n if (!fileMatches(mediaType, file)) {\n setError(\n mediaType === RESIDUAL_KIND\n ? `This field takes a document or other file — “${file.name}” is an image or a video.`\n : `This field takes ${mediaType} files — “${file.name}” is not one.`,\n )\n return\n }\n setIsUploading(true)\n try {\n const uploaded = await uploadMedia(file)\n // The server classifies the stored file; trust that over the browser's\n // guess from the extension.\n const mismatch = typeMismatch(mediaType, uploaded)\n if (mismatch !== null) {\n setError(mismatch)\n return\n }\n\n // Single mode: auto-confirm the just-uploaded file\n if (mode === 'single') {\n onSelect([uploaded])\n onOpenChange(false)\n return\n }\n\n // Multi mode: add to grid and select\n setItems((prev) => [uploaded, ...prev])\n setTotal((prev) => prev + 1)\n setSelected((prev) => new Set([...prev, uploaded.id]))\n } catch (err) {\n setError(\n err instanceof Error && err.message\n ? `Upload failed: ${err.message}`\n : 'Upload failed. Please try again.',\n )\n } finally {\n setIsUploading(false)\n }\n },\n [uploadMedia, mode, mediaType, onSelect, onOpenChange],\n )\n\n return (\n <DialogPrimitive.Root open={open} onOpenChange={onOpenChange}>\n <DialogPrimitive.Portal>\n <DialogPrimitive.Overlay\n className={cn(\n 'fixed inset-0 z-50 bg-black/50 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0',\n classNames?.overlay,\n )}\n />\n <DialogPrimitive.Content\n {...(!description && { 'aria-describedby': undefined })}\n className={cn(\n 'fixed left-1/2 top-1/2 z-50 -translate-x-1/2 -translate-y-1/2',\n 'flex max-h-[85vh] w-[90vw] max-w-4xl flex-col',\n 'rounded-xl border border-zinc-200 bg-white shadow-2xl dark:border-zinc-800 dark:bg-zinc-950',\n classNames?.content,\n className,\n )}\n >\n {/* Header */}\n <div\n className={cn(\n 'flex items-center justify-between border-b border-zinc-200 px-6 py-4 dark:border-zinc-800',\n classNames?.header,\n )}\n >\n <DialogPrimitive.Title\n className={cn(\n 'text-lg font-semibold text-zinc-900 dark:text-zinc-50',\n classNames?.title,\n )}\n >\n {title}\n </DialogPrimitive.Title>\n {description && (\n <VisuallyHidden.Root asChild>\n <DialogPrimitive.Description>{description}</DialogPrimitive.Description>\n </VisuallyHidden.Root>\n )}\n <DialogPrimitive.Close className=\"rounded-sm opacity-70 transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-zinc-400 dark:focus:ring-zinc-600\">\n <X className=\"h-4 w-4\" />\n <VisuallyHidden.Root>Close</VisuallyHidden.Root>\n </DialogPrimitive.Close>\n </div>\n\n {/* Toolbar */}\n <div\n className={cn(\n 'border-b border-zinc-200 px-6 py-3 dark:border-zinc-800',\n classNames?.toolbar,\n )}\n >\n <SearchBar\n value={search}\n onChange={setSearch}\n mediaType={effectiveMediaType}\n onMediaTypeChange={setMediaTypeFilter}\n locked={!!mediaType}\n classNames={classNames}\n />\n </div>\n\n {/* Content */}\n <div className=\"flex-1 overflow-y-auto px-6 py-4\">\n {error && (\n <div\n role=\"alert\"\n className=\"mb-4 rounded-lg border border-red-200 bg-red-50 px-4 py-3 text-sm text-red-700 dark:border-red-900 dark:bg-red-950/50 dark:text-red-400\"\n >\n {error}\n </div>\n )}\n {storageDisabled ? (\n <div\n role=\"alert\"\n className=\"rounded-lg border border-amber-200 bg-amber-50 px-4 py-6 text-center dark:border-amber-900 dark:bg-amber-950/30\"\n >\n <p className=\"text-sm font-medium text-amber-900 dark:text-amber-200\">\n Media storage is not configured\n </p>\n <p className=\"mt-1 text-xs text-amber-800 dark:text-amber-300\">{storageDisabled}</p>\n <p className=\"mt-3 text-xs text-amber-700 dark:text-amber-400\">\n Set the required environment variables and restart the app to enable uploads.\n </p>\n </div>\n ) : (\n <>\n <UploadZone\n onUpload={handleUpload}\n isUploading={isUploading}\n accept={effectiveAccept}\n classNames={classNames}\n />\n <MediaGrid\n items={items}\n selected={selected}\n onToggle={handleToggle}\n isLoading={isLoading}\n total={total}\n offset={offset}\n limit={ITEMS_PER_PAGE}\n onPageChange={setOffset}\n classNames={classNames}\n />\n </>\n )}\n </div>\n\n {/* Footer — multi mode shows confirm/cancel, single mode shows item count only */}\n <div\n className={cn(\n 'flex items-center justify-between border-t border-zinc-200 px-6 py-4 dark:border-zinc-800',\n classNames?.footer,\n )}\n >\n <span className=\"text-sm text-zinc-500 dark:text-zinc-400\">\n {mode === 'single'\n ? `${total.toString()} item${total !== 1 ? 's' : ''} — click to select`\n : selected.size > 0\n ? `${selected.size.toString()} selected`\n : `${total.toString()} item${total !== 1 ? 's' : ''}`}\n </span>\n <div className=\"flex gap-2\">\n {children}\n <DialogPrimitive.Close asChild>\n <button\n type=\"button\"\n className={cn(\n 'rounded-md border border-zinc-300 px-4 py-2 text-sm font-medium text-zinc-700 hover:bg-zinc-50',\n 'dark:border-zinc-700 dark:text-zinc-300 dark:hover:bg-zinc-900',\n classNames?.cancelButton,\n )}\n >\n Cancel\n </button>\n </DialogPrimitive.Close>\n {mode !== 'single' && (\n <button\n type=\"button\"\n onClick={handleConfirm}\n disabled={selected.size === 0}\n className={cn(\n 'rounded-md bg-blue-600 px-4 py-2 text-sm font-medium text-white hover:bg-blue-500',\n 'disabled:cursor-not-allowed disabled:opacity-50',\n classNames?.confirmButton,\n )}\n >\n {`Select (${selected.size.toString()})`}\n </button>\n )}\n </div>\n </div>\n </DialogPrimitive.Content>\n </DialogPrimitive.Portal>\n </DialogPrimitive.Root>\n )\n}\n"],"mappings":"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"}
|
package/dist/plugin.d.mts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { a as ImageVocabulary, c as MediaPluginConfig, i as ImageStyle } from "./types-CgkJF5dc.mjs";
|
|
2
2
|
import { getMediaMessages } from "./i18n.mjs";
|
|
3
3
|
|
|
4
4
|
//#region src/plugin.d.ts
|
|
@@ -26,6 +26,7 @@ declare function media(config?: MediaPluginConfig): Pick<import("@murumets-ee/co
|
|
|
26
26
|
readonly shared?: {
|
|
27
27
|
readonly messages: typeof getMediaMessages;
|
|
28
28
|
readonly settings: readonly [import("@murumets-ee/settings").SettingsDefinition<{
|
|
29
|
+
readonly vocabulary: import("@murumets-ee/settings").JsonSettingConfig<ImageVocabulary>;
|
|
29
30
|
readonly imageStyles: import("@murumets-ee/settings").JsonSettingConfig<Record<string, ImageStyle>>;
|
|
30
31
|
}, "media.imageStyles">];
|
|
31
32
|
};
|
package/dist/plugin.d.mts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"plugin.d.mts","names":[],"sources":["../src/plugin.ts"],"mappings":";;;;;;;;;;;
|
|
1
|
+
{"version":3,"file":"plugin.d.mts","names":[],"sources":["../src/plugin.ts"],"mappings":";;;;;;;;;;;iBA6CgB,cAAA,CAAA,GAAkB,QAAQ,CAAC,iBAAA;;;;;;;;;;;;iBAe3B,KAAA,CAAM,MAAA,GAAS,iBAAA,GAAiB,IAAA,6BAAA,MAAA;EAAA"}
|