@openrails/contentkit-ui 0.0.0-stage → 0.68.1
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/README.md +470 -2
- package/dist/appearance.d.ts +38 -0
- package/dist/appearance.js +15 -0
- package/dist/appearance.js.map +1 -0
- package/dist/assets/hash.worker-BYfo-7MQ.js.map +1 -0
- package/dist/client/aspect.d.ts +6 -0
- package/dist/client/aspect.js +17 -0
- package/dist/client/aspect.js.map +1 -0
- package/dist/client/client.d.ts +53 -0
- package/dist/client/client.js +36 -0
- package/dist/client/client.js.map +1 -0
- package/dist/client/codes.d.ts +12 -0
- package/dist/client/codes.js +19 -0
- package/dist/client/codes.js.map +1 -0
- package/dist/client/content/bans.d.ts +15 -0
- package/dist/client/content/bans.js +40 -0
- package/dist/client/content/bans.js.map +1 -0
- package/dist/client/content/comments.d.ts +36 -0
- package/dist/client/content/comments.js +95 -0
- package/dist/client/content/comments.js.map +1 -0
- package/dist/client/content/favorites.d.ts +14 -0
- package/dist/client/content/favorites.js +42 -0
- package/dist/client/content/favorites.js.map +1 -0
- package/dist/client/content/moderation.d.ts +18 -0
- package/dist/client/content/moderation.js +37 -0
- package/dist/client/content/moderation.js.map +1 -0
- package/dist/client/content/polls.d.ts +47 -0
- package/dist/client/content/polls.js +151 -0
- package/dist/client/content/polls.js.map +1 -0
- package/dist/client/content/posts.d.ts +44 -0
- package/dist/client/content/posts.js +92 -0
- package/dist/client/content/posts.js.map +1 -0
- package/dist/client/content/reactions.d.ts +12 -0
- package/dist/client/content/reactions.js +36 -0
- package/dist/client/content/reactions.js.map +1 -0
- package/dist/client/content/types.d.ts +91 -0
- package/dist/client/content/types.js +8 -0
- package/dist/client/content/types.js.map +1 -0
- package/dist/client/crop.d.ts +32 -0
- package/dist/client/crop.js +103 -0
- package/dist/client/crop.js.map +1 -0
- package/dist/client/errors.d.ts +54 -0
- package/dist/client/errors.js +80 -0
- package/dist/client/errors.js.map +1 -0
- package/dist/client/gallery.d.ts +43 -0
- package/dist/client/gallery.js +86 -0
- package/dist/client/gallery.js.map +1 -0
- package/dist/client/generated/error-codes.d.ts +5 -0
- package/dist/client/generated/error-codes.js +131 -0
- package/dist/client/generated/error-codes.js.map +1 -0
- package/dist/client/generated/routes.d.ts +1086 -0
- package/dist/client/generated/routes.js +1400 -0
- package/dist/client/generated/routes.js.map +1 -0
- package/dist/client/generated/wire.d.ts +630 -0
- package/dist/client/generated/wire.js +6 -0
- package/dist/client/generated/wire.js.map +1 -0
- package/dist/client/http.d.ts +83 -0
- package/dist/client/http.js +82 -0
- package/dist/client/http.js.map +1 -0
- package/dist/client/image.d.ts +24 -0
- package/dist/client/image.js +65 -0
- package/dist/client/image.js.map +1 -0
- package/dist/client/index.d.ts +33 -0
- package/dist/client/media/api.d.ts +36 -0
- package/dist/client/media/api.js +61 -0
- package/dist/client/media/api.js.map +1 -0
- package/dist/client/media/client.d.ts +247 -0
- package/dist/client/media/client.js +407 -0
- package/dist/client/media/client.js.map +1 -0
- package/dist/client/media/encode.d.ts +3 -0
- package/dist/client/media/encode.js +8 -0
- package/dist/client/media/encode.js.map +1 -0
- package/dist/client/media/hash-stream.d.ts +6 -0
- package/dist/client/media/hash-stream.js +16 -0
- package/dist/client/media/hash-stream.js.map +1 -0
- package/dist/client/media/hash.d.ts +12 -0
- package/dist/client/media/hash.js +48 -0
- package/dist/client/media/hash.js.map +1 -0
- package/dist/client/media/hash.worker-inline.js +18 -0
- package/dist/client/media/hash.worker-inline.js.map +1 -0
- package/dist/client/media/pacer.d.ts +21 -0
- package/dist/client/media/pacer.js +25 -0
- package/dist/client/media/pacer.js.map +1 -0
- package/dist/client/media/queue.d.ts +112 -0
- package/dist/client/media/queue.js +274 -0
- package/dist/client/media/queue.js.map +1 -0
- package/dist/client/media/ref.d.ts +6 -0
- package/dist/client/media/ref.js +10 -0
- package/dist/client/media/ref.js.map +1 -0
- package/dist/client/media/rules.d.ts +54 -0
- package/dist/client/media/rules.js +143 -0
- package/dist/client/media/rules.js.map +1 -0
- package/dist/client/media/transport.d.ts +13 -0
- package/dist/client/media/transport.js +33 -0
- package/dist/client/media/transport.js.map +1 -0
- package/dist/client/media/windows.d.ts +15 -0
- package/dist/client/media/windows.js +39 -0
- package/dist/client/media/windows.js.map +1 -0
- package/dist/client/playback.d.ts +200 -0
- package/dist/client/playback.js +142 -0
- package/dist/client/playback.js.map +1 -0
- package/dist/client/player.d.ts +102 -0
- package/dist/client/player.js +155 -0
- package/dist/client/player.js.map +1 -0
- package/dist/client/public.d.ts +25 -0
- package/dist/client/public.js +18 -0
- package/dist/client/public.js.map +1 -0
- package/dist/client/rendition.d.ts +19 -0
- package/dist/client/rendition.js +16 -0
- package/dist/client/rendition.js.map +1 -0
- package/dist/client/route.d.ts +39 -0
- package/dist/client/route.js +31 -0
- package/dist/client/route.js.map +1 -0
- package/dist/client/taxonomy.d.ts +30 -0
- package/dist/client/taxonomy.js +77 -0
- package/dist/client/taxonomy.js.map +1 -0
- package/dist/client.js +33 -0
- package/dist/components/encode-progress.d.ts +15 -0
- package/dist/components/encode-progress.js +49 -0
- package/dist/components/encode-progress.js.map +1 -0
- package/dist/components/hover-preview.d.ts +29 -0
- package/dist/components/hover-preview.js +49 -0
- package/dist/components/hover-preview.js.map +1 -0
- package/dist/components/image-crop-dialog.d.ts +48 -0
- package/dist/components/image-crop-dialog.js +359 -0
- package/dist/components/image-crop-dialog.js.map +1 -0
- package/dist/components/lazy-mount.d.ts +10 -0
- package/dist/components/lazy-mount.js +22 -0
- package/dist/components/lazy-mount.js.map +1 -0
- package/dist/components/media-folder-editor.d.ts +59 -0
- package/dist/components/media-folder-editor.js +611 -0
- package/dist/components/media-folder-editor.js.map +1 -0
- package/dist/components/media-gallery.d.ts +54 -0
- package/dist/components/media-gallery.js +597 -0
- package/dist/components/media-gallery.js.map +1 -0
- package/dist/components/media-readiness-notice.d.ts +24 -0
- package/dist/components/media-readiness-notice.js +61 -0
- package/dist/components/media-readiness-notice.js.map +1 -0
- package/dist/components/player-controls.d.ts +71 -0
- package/dist/components/player-controls.js +264 -0
- package/dist/components/player-controls.js.map +1 -0
- package/dist/components/progress.d.ts +5 -0
- package/dist/components/progress.js +14 -0
- package/dist/components/progress.js.map +1 -0
- package/dist/components/rendition-img.d.ts +34 -0
- package/dist/components/rendition-img.js +78 -0
- package/dist/components/rendition-img.js.map +1 -0
- package/dist/components/slot-editor.d.ts +95 -0
- package/dist/components/slot-editor.js +182 -0
- package/dist/components/slot-editor.js.map +1 -0
- package/dist/components/slot-image.d.ts +23 -0
- package/dist/components/slot-image.js +48 -0
- package/dist/components/slot-image.js.map +1 -0
- package/dist/components/slot-upload.d.ts +41 -0
- package/dist/components/slot-upload.js +215 -0
- package/dist/components/slot-upload.js.map +1 -0
- package/dist/components/social/bans.d.ts +42 -0
- package/dist/components/social/bans.js +300 -0
- package/dist/components/social/bans.js.map +1 -0
- package/dist/components/social/comments.d.ts +42 -0
- package/dist/components/social/comments.js +517 -0
- package/dist/components/social/comments.js.map +1 -0
- package/dist/components/social/engagement.d.ts +45 -0
- package/dist/components/social/engagement.js +129 -0
- package/dist/components/social/engagement.js.map +1 -0
- package/dist/components/social/index.d.ts +6 -0
- package/dist/components/social/moderation.d.ts +32 -0
- package/dist/components/social/moderation.js +383 -0
- package/dist/components/social/moderation.js.map +1 -0
- package/dist/components/social/parts.d.ts +50 -0
- package/dist/components/social/parts.js +106 -0
- package/dist/components/social/parts.js.map +1 -0
- package/dist/components/social/poll-editor.d.ts +34 -0
- package/dist/components/social/poll-editor.js +567 -0
- package/dist/components/social/poll-editor.js.map +1 -0
- package/dist/components/social/poll.d.ts +30 -0
- package/dist/components/social/poll.js +252 -0
- package/dist/components/social/poll.js.map +1 -0
- package/dist/components/sortable-list.d.ts +29 -0
- package/dist/components/sortable-list.js +121 -0
- package/dist/components/sortable-list.js.map +1 -0
- package/dist/components/video-mini-player.d.ts +35 -0
- package/dist/components/video-mini-player.js +236 -0
- package/dist/components/video-mini-player.js.map +1 -0
- package/dist/components/video-player.d.ts +146 -0
- package/dist/components/video-player.js +630 -0
- package/dist/components/video-player.js.map +1 -0
- package/dist/components/video-poster-picker.d.ts +59 -0
- package/dist/components/video-poster-picker.js +335 -0
- package/dist/components/video-poster-picker.js.map +1 -0
- package/dist/components/video-poster.d.ts +24 -0
- package/dist/components/video-poster.js +46 -0
- package/dist/components/video-poster.js.map +1 -0
- package/dist/i18n/context.d.ts +4 -0
- package/dist/i18n/context.js +11 -0
- package/dist/i18n/context.js.map +1 -0
- package/dist/i18n/messages.d.ts +40 -0
- package/dist/i18n/messages.js +94 -0
- package/dist/i18n/messages.js.map +1 -0
- package/dist/index.d.ts +23 -0
- package/dist/index.js +41 -0
- package/dist/locales/de.d.ts +2 -0
- package/dist/locales/de.js +271 -0
- package/dist/locales/de.js.map +1 -0
- package/dist/locales/en.d.ts +457 -0
- package/dist/locales/en.js +271 -0
- package/dist/locales/en.js.map +1 -0
- package/dist/locales/es.d.ts +2 -0
- package/dist/locales/es.js +271 -0
- package/dist/locales/es.js.map +1 -0
- package/dist/locales/ja.d.ts +2 -0
- package/dist/locales/ja.js +271 -0
- package/dist/locales/ja.js.map +1 -0
- package/dist/locales/ko.d.ts +2 -0
- package/dist/locales/ko.js +271 -0
- package/dist/locales/ko.js.map +1 -0
- package/dist/locales/social/de.d.ts +2 -0
- package/dist/locales/social/de.js +198 -0
- package/dist/locales/social/de.js.map +1 -0
- package/dist/locales/social/en.d.ts +195 -0
- package/dist/locales/social/en.js +198 -0
- package/dist/locales/social/en.js.map +1 -0
- package/dist/locales/social/es.d.ts +2 -0
- package/dist/locales/social/es.js +198 -0
- package/dist/locales/social/es.js.map +1 -0
- package/dist/locales/social/ja.d.ts +2 -0
- package/dist/locales/social/ja.js +198 -0
- package/dist/locales/social/ja.js.map +1 -0
- package/dist/locales/social/ko.d.ts +2 -0
- package/dist/locales/social/ko.js +198 -0
- package/dist/locales/social/ko.js.map +1 -0
- package/dist/locales/social/zh.d.ts +2 -0
- package/dist/locales/social/zh.js +198 -0
- package/dist/locales/social/zh.js.map +1 -0
- package/dist/locales/zh.d.ts +2 -0
- package/dist/locales/zh.js +271 -0
- package/dist/locales/zh.js.map +1 -0
- package/dist/provider.d.ts +22 -0
- package/dist/provider.js +59 -0
- package/dist/provider.js.map +1 -0
- package/dist/react/canonical.d.ts +33 -0
- package/dist/react/canonical.js +78 -0
- package/dist/react/canonical.js.map +1 -0
- package/dist/react/comments.d.ts +82 -0
- package/dist/react/comments.js +155 -0
- package/dist/react/comments.js.map +1 -0
- package/dist/react/config.d.ts +13 -0
- package/dist/react/config.js +14 -0
- package/dist/react/config.js.map +1 -0
- package/dist/react/context.d.ts +55 -0
- package/dist/react/context.js +42 -0
- package/dist/react/context.js.map +1 -0
- package/dist/react/crop.d.ts +49 -0
- package/dist/react/crop.js +107 -0
- package/dist/react/crop.js.map +1 -0
- package/dist/react/engagement.d.ts +38 -0
- package/dist/react/engagement.js +88 -0
- package/dist/react/engagement.js.map +1 -0
- package/dist/react/folder.d.ts +103 -0
- package/dist/react/folder.js +240 -0
- package/dist/react/folder.js.map +1 -0
- package/dist/react/gallery.d.ts +140 -0
- package/dist/react/gallery.js +477 -0
- package/dist/react/gallery.js.map +1 -0
- package/dist/react/index.d.ts +21 -0
- package/dist/react/inline-preview.d.ts +24 -0
- package/dist/react/inline-preview.js +76 -0
- package/dist/react/inline-preview.js.map +1 -0
- package/dist/react/player.d.ts +70 -0
- package/dist/react/player.js +214 -0
- package/dist/react/player.js.map +1 -0
- package/dist/react/polls.d.ts +74 -0
- package/dist/react/polls.js +176 -0
- package/dist/react/polls.js.map +1 -0
- package/dist/react/posts.d.ts +59 -0
- package/dist/react/posts.js +149 -0
- package/dist/react/posts.js.map +1 -0
- package/dist/react/provider.d.ts +27 -0
- package/dist/react/provider.js +49 -0
- package/dist/react/provider.js.map +1 -0
- package/dist/react/public.d.ts +39 -0
- package/dist/react/public.js +70 -0
- package/dist/react/public.js.map +1 -0
- package/dist/react/read.d.ts +52 -0
- package/dist/react/read.js +144 -0
- package/dist/react/read.js.map +1 -0
- package/dist/react/resources.d.ts +73 -0
- package/dist/react/resources.js +247 -0
- package/dist/react/resources.js.map +1 -0
- package/dist/react/slot.d.ts +104 -0
- package/dist/react/slot.js +185 -0
- package/dist/react/slot.js.map +1 -0
- package/dist/react/store.d.ts +51 -0
- package/dist/react/store.js +165 -0
- package/dist/react/store.js.map +1 -0
- package/dist/react/upload.d.ts +86 -0
- package/dist/react/upload.js +155 -0
- package/dist/react/upload.js.map +1 -0
- package/dist/react/use-resource.d.ts +44 -0
- package/dist/react/use-resource.js +74 -0
- package/dist/react/use-resource.js.map +1 -0
- package/dist/react/video.d.ts +118 -0
- package/dist/react/video.js +184 -0
- package/dist/react/video.js.map +1 -0
- package/dist/react/viewport.d.ts +19 -0
- package/dist/react/viewport.js +30 -0
- package/dist/react/viewport.js.map +1 -0
- package/dist/react.js +21 -0
- package/dist/scope.d.ts +14 -0
- package/dist/scope.js +30 -0
- package/dist/scope.js.map +1 -0
- package/dist/styles.css +3 -0
- package/dist/ui/alert-dialog.d.ts +18 -0
- package/dist/ui/alert-dialog.js +86 -0
- package/dist/ui/alert-dialog.js.map +1 -0
- package/dist/ui/alert.d.ts +10 -0
- package/dist/ui/alert.js +38 -0
- package/dist/ui/alert.js.map +1 -0
- package/dist/ui/avatar.d.ts +11 -0
- package/dist/ui/avatar.js +31 -0
- package/dist/ui/avatar.js.map +1 -0
- package/dist/ui/badge.d.ts +7 -0
- package/dist/ui/badge.js +31 -0
- package/dist/ui/badge.js.map +1 -0
- package/dist/ui/button.d.ts +8 -0
- package/dist/ui/button.js +46 -0
- package/dist/ui/button.js.map +1 -0
- package/dist/ui/dialog.d.ts +17 -0
- package/dist/ui/dialog.js +91 -0
- package/dist/ui/dialog.js.map +1 -0
- package/dist/ui/dropdown-menu.d.ts +15 -0
- package/dist/ui/dropdown-menu.js +105 -0
- package/dist/ui/dropdown-menu.js.map +1 -0
- package/dist/ui/input.d.ts +3 -0
- package/dist/ui/input.js +17 -0
- package/dist/ui/input.js.map +1 -0
- package/dist/ui/label.d.ts +3 -0
- package/dist/ui/label.js +15 -0
- package/dist/ui/label.js.map +1 -0
- package/dist/ui/progress.d.ts +7 -0
- package/dist/ui/progress.js +31 -0
- package/dist/ui/progress.js.map +1 -0
- package/dist/ui/select.d.ts +15 -0
- package/dist/ui/select.js +98 -0
- package/dist/ui/select.js.map +1 -0
- package/dist/ui/skeleton.d.ts +2 -0
- package/dist/ui/skeleton.js +14 -0
- package/dist/ui/skeleton.js.map +1 -0
- package/dist/ui/slider.d.ts +3 -0
- package/dist/ui/slider.js +35 -0
- package/dist/ui/slider.js.map +1 -0
- package/dist/ui/switch.d.ts +5 -0
- package/dist/ui/switch.js +20 -0
- package/dist/ui/switch.js.map +1 -0
- package/dist/ui/tabs.d.ts +10 -0
- package/dist/ui/tabs.js +47 -0
- package/dist/ui/tabs.js.map +1 -0
- package/dist/ui/textarea.d.ts +3 -0
- package/dist/ui/textarea.js +15 -0
- package/dist/ui/textarea.js.map +1 -0
- package/dist/ui/toggle-group.d.ts +5 -0
- package/dist/ui/toggle-group.js +23 -0
- package/dist/ui/toggle-group.js.map +1 -0
- package/dist/urls/index.d.ts +100 -0
- package/dist/urls.js +118 -0
- package/dist/urls.js.map +1 -0
- package/package.json +120 -3
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { ComponentProps, ReactNode } from 'react';
|
|
2
|
+
import { DensityRange, Rendition } from '../client/rendition.js';
|
|
3
|
+
export declare const DensityContext: import('react').Context<DensityRange>;
|
|
4
|
+
export interface UseRenditionOptions {
|
|
5
|
+
/** Density range; default the provider's (2–3×). */
|
|
6
|
+
density?: DensityRange;
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* Picks a rendition for the element `ref` (a callback ref) is attached to: the narrowest at
|
|
10
|
+
* least its rendered width × density. It re-picks on resize (fullscreen,
|
|
11
|
+
* window changes), never steps down, and keeps the shown rendition until a
|
|
12
|
+
* wider one has loaded, so nothing flickers.
|
|
13
|
+
*/
|
|
14
|
+
export declare function useRendition<T extends Rendition, E extends HTMLElement = HTMLImageElement>(outputs: readonly T[] | null | undefined, o?: UseRenditionOptions): {
|
|
15
|
+
ref: import('react').Dispatch<import('react').SetStateAction<E | null>>;
|
|
16
|
+
rendition: T | undefined;
|
|
17
|
+
onLoad: () => void;
|
|
18
|
+
};
|
|
19
|
+
export interface RenditionImgProps extends Omit<ComponentProps<"img">, "src" | "srcSet" | "sizes" | "width" | "height"> {
|
|
20
|
+
outputs: readonly Rendition[] | null | undefined;
|
|
21
|
+
density?: DensityRange;
|
|
22
|
+
/** Rendered instead when there are no renditions or the image fails to load (a 404). */
|
|
23
|
+
fallback?: ReactNode;
|
|
24
|
+
/** A pulsing muted fill until the image has loaded. */
|
|
25
|
+
skeleton?: boolean;
|
|
26
|
+
/** object-fit of the image in its box. */
|
|
27
|
+
fit?: "cover" | "contain";
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* An `<img>` showing the rendition its rendered size × density needs (see
|
|
31
|
+
* useRendition), with a fallback for none or a failed load and an optional
|
|
32
|
+
* loading skeleton.
|
|
33
|
+
*/
|
|
34
|
+
export declare function RenditionImg({ outputs, density, onLoad, onError, alt, fallback, skeleton, fit, style, ...img }: RenditionImgProps): import('react').JSX.Element;
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { DEFAULT_DENSITY as e, densityFor as t, pickRendition as n, sortRenditions as r } from "../client/rendition.js";
|
|
2
|
+
import { useReducedMotion as i } from "../react/inline-preview.js";
|
|
3
|
+
import { createContext as a, useCallback as o, useContext as s, useEffect as c, useLayoutEffect as l, useMemo as u, useRef as d, useState as f } from "react";
|
|
4
|
+
import { Fragment as p, jsx as m } from "react/jsx-runtime";
|
|
5
|
+
//#region src/components/rendition-img.tsx
|
|
6
|
+
var h = a(e), g = typeof window > "u" ? c : l;
|
|
7
|
+
function _(e, i = {}) {
|
|
8
|
+
let a = s(h), l = i.density ?? a, [p, m] = f(null), [_, v] = f(-1);
|
|
9
|
+
g(() => {
|
|
10
|
+
if (!p) return;
|
|
11
|
+
let e = () => v((e) => Math.max(e, p.getBoundingClientRect().width));
|
|
12
|
+
if (e(), typeof ResizeObserver != "function") return;
|
|
13
|
+
let t = new ResizeObserver(e);
|
|
14
|
+
return t.observe(p), () => t.disconnect();
|
|
15
|
+
}, [p]);
|
|
16
|
+
let y = u(() => r(e), [e]), b = _ >= 0 ? n(y, _, t(l)) : void 0, [x, S] = f(void 0), C = d(!1), w = x && y.some((e) => e.url === x.url) ? x : void 0;
|
|
17
|
+
c(() => {
|
|
18
|
+
if (!b || b.url === w?.url) return;
|
|
19
|
+
if (!w || !C.current) {
|
|
20
|
+
C.current = !1, S(b);
|
|
21
|
+
return;
|
|
22
|
+
}
|
|
23
|
+
if (b.w <= w.w) return;
|
|
24
|
+
let e = !0, t = new Image();
|
|
25
|
+
return t.onload = () => e && S(b), t.src = b.url, () => {
|
|
26
|
+
e = !1;
|
|
27
|
+
};
|
|
28
|
+
}, [b, w]);
|
|
29
|
+
let T = o(() => {
|
|
30
|
+
C.current = !0;
|
|
31
|
+
}, []);
|
|
32
|
+
return {
|
|
33
|
+
ref: m,
|
|
34
|
+
rendition: w ?? b,
|
|
35
|
+
onLoad: T
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
var v = { backgroundColor: "var(--ckui-muted, rgb(127 127 127 / 0.15))" }, y = "ckui-pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite";
|
|
39
|
+
function b({ outputs: e, density: t, onLoad: n, onError: a, alt: s = "", fallback: c, skeleton: l, fit: u, style: d, ...h }) {
|
|
40
|
+
let { ref: b, rendition: x, onLoad: S } = _(e, { density: t }), C = r(e)[0], w = x ?? C, T = i(), [E, D] = f(null), [O, k] = f(null), [A, j] = f(null), M = x ? w?.url : void 0;
|
|
41
|
+
g(() => {
|
|
42
|
+
A && M && A.complete && A.naturalWidth > 0 && k(M);
|
|
43
|
+
}, [A, M]);
|
|
44
|
+
let N = o((e) => {
|
|
45
|
+
b(e), j(e);
|
|
46
|
+
}, [b]);
|
|
47
|
+
if (!w || C && E === C.url) return /* @__PURE__ */ m(p, { children: c ?? null });
|
|
48
|
+
let P = l && O !== M;
|
|
49
|
+
return /* @__PURE__ */ m("img", {
|
|
50
|
+
...h,
|
|
51
|
+
ref: N,
|
|
52
|
+
alt: s,
|
|
53
|
+
src: M,
|
|
54
|
+
width: w.w,
|
|
55
|
+
height: w.h || void 0,
|
|
56
|
+
decoding: "async",
|
|
57
|
+
"data-rendition": x ? w.w : void 0,
|
|
58
|
+
"data-loading": P ? "" : void 0,
|
|
59
|
+
style: {
|
|
60
|
+
...u ? { objectFit: u } : {},
|
|
61
|
+
...P ? {
|
|
62
|
+
...v,
|
|
63
|
+
animation: T ? void 0 : y
|
|
64
|
+
} : {},
|
|
65
|
+
...d
|
|
66
|
+
},
|
|
67
|
+
onLoad: (e) => {
|
|
68
|
+
S(), M && k(M), n?.(e);
|
|
69
|
+
},
|
|
70
|
+
onError: (e) => {
|
|
71
|
+
C && D(C.url), a?.(e);
|
|
72
|
+
}
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
//#endregion
|
|
76
|
+
export { h as DensityContext, b as RenditionImg, _ as useRendition };
|
|
77
|
+
|
|
78
|
+
//# sourceMappingURL=rendition-img.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"rendition-img.js","names":[],"sources":["../../src/components/rendition-img.tsx"],"sourcesContent":["import { createContext, useCallback, useContext, useEffect, useLayoutEffect, useMemo, useRef, useState, type ComponentProps, type CSSProperties, type ReactNode } from \"react\";\nimport { DEFAULT_DENSITY, densityFor, pickRendition, sortRenditions, type DensityRange, type Rendition } from \"../client/rendition.js\";\nimport { useReducedMotion } from \"../react/inline-preview.js\";\n\nexport const DensityContext = createContext<DensityRange>(DEFAULT_DENSITY);\n\nconst useIsoLayoutEffect = typeof window === \"undefined\" ? useEffect : useLayoutEffect;\n\nexport interface UseRenditionOptions {\n /** Density range; default the provider's (2–3×). */\n density?: DensityRange;\n}\n\n/**\n * Picks a rendition for the element `ref` (a callback ref) is attached to: the narrowest at\n * least its rendered width × density. It re-picks on resize (fullscreen,\n * window changes), never steps down, and keeps the shown rendition until a\n * wider one has loaded, so nothing flickers.\n */\nexport function useRendition<T extends Rendition, E extends HTMLElement = HTMLImageElement>(outputs: readonly T[] | null | undefined, o: UseRenditionOptions = {}) {\n const provided = useContext(DensityContext);\n const range = o.density ?? provided;\n const [el, ref] = useState<E | null>(null);\n // -1 until measured; a hidden element measures 0 and gets the narrowest.\n const [width, setWidth] = useState(-1);\n useIsoLayoutEffect(() => {\n if (!el) return;\n const measure = () => setWidth((w) => Math.max(w, el.getBoundingClientRect().width));\n measure();\n if (typeof ResizeObserver !== \"function\") return;\n const ro = new ResizeObserver(measure);\n ro.observe(el);\n return () => ro.disconnect();\n }, [el]);\n const sorted = useMemo(() => sortRenditions(outputs), [outputs]);\n const want = width >= 0 ? pickRendition(sorted, width, densityFor(range)) : undefined;\n const [shown, setShown] = useState<T | undefined>(undefined);\n const loaded = useRef(false);\n const current = shown && sorted.some((r) => r.url === shown.url) ? shown : undefined;\n useEffect(() => {\n if (!want || want.url === current?.url) return;\n // First pick, a new set, or before anything loaded: show it now.\n if (!current || !loaded.current) {\n loaded.current = false;\n setShown(want);\n return;\n }\n if (want.w <= current.w) return;\n let live = true;\n const img = new Image();\n img.onload = () => live && setShown(want);\n img.src = want.url;\n return () => {\n live = false;\n };\n }, [want, current]);\n const onLoad = useCallback(() => {\n loaded.current = true;\n }, []);\n return { ref, rendition: current ?? want, onLoad };\n}\n\nexport interface RenditionImgProps extends Omit<ComponentProps<\"img\">, \"src\" | \"srcSet\" | \"sizes\" | \"width\" | \"height\"> {\n outputs: readonly Rendition[] | null | undefined;\n density?: DensityRange;\n /** Rendered instead when there are no renditions or the image fails to load (a 404). */\n fallback?: ReactNode;\n /** A pulsing muted fill until the image has loaded. */\n skeleton?: boolean;\n /** object-fit of the image in its box. */\n fit?: \"cover\" | \"contain\";\n}\n\nconst SKELETON: CSSProperties = { backgroundColor: \"var(--ckui-muted, rgb(127 127 127 / 0.15))\" };\nconst PULSE = \"ckui-pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite\";\n\n/**\n * An `<img>` showing the rendition its rendered size × density needs (see\n * useRendition), with a fallback for none or a failed load and an optional\n * loading skeleton.\n */\nexport function RenditionImg({ outputs, density, onLoad, onError, alt = \"\", fallback, skeleton, fit, style, ...img }: RenditionImgProps) {\n const { ref, rendition, onLoad: loaded } = useRendition(outputs, { density });\n const first = sortRenditions(outputs)[0];\n const r = rendition ?? first;\n const reduced = useReducedMotion();\n const [failed, setFailed] = useState<string | null>(null);\n const [done, setDone] = useState<string | null>(null);\n const [el, setEl] = useState<HTMLImageElement | null>(null);\n const src = rendition ? r?.url : undefined;\n // A cached image is complete before its load event; settle it before paint.\n useIsoLayoutEffect(() => {\n if (el && src && el.complete && el.naturalWidth > 0) setDone(src);\n }, [el, src]);\n const attach = useCallback(\n (node: HTMLImageElement | null) => {\n ref(node);\n setEl(node);\n },\n [ref],\n );\n if (!r || (first && failed === first.url)) return <>{fallback ?? null}</>;\n const loading = skeleton && done !== src;\n return (\n <img\n {...img}\n ref={attach}\n alt={alt}\n src={src}\n width={r.w}\n height={r.h || undefined}\n decoding=\"async\"\n data-rendition={rendition ? r.w : undefined}\n data-loading={loading ? \"\" : undefined}\n style={{ ...(fit ? { objectFit: fit } : {}), ...(loading ? { ...SKELETON, animation: reduced ? undefined : PULSE } : {}), ...style }}\n onLoad={(e) => {\n loaded();\n if (src) setDone(src);\n onLoad?.(e);\n }}\n onError={(e) => {\n if (first) setFailed(first.url);\n onError?.(e);\n }}\n />\n );\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import { AspectRatio } from '../client/aspect.js';
|
|
2
|
+
import { ReactElement, ReactNode } from 'react';
|
|
3
|
+
import { ContentKitClient } from '../client/client.js';
|
|
4
|
+
import { CropSource } from '../client/image.js';
|
|
5
|
+
import { ContentKitErrorHandler } from '../react/context.js';
|
|
6
|
+
import { PublicPreset } from '../client/public.js';
|
|
7
|
+
import { UseSlotCrop, UseSlotImage } from '../react/slot.js';
|
|
8
|
+
import { FileInfo, ReadResult, RefBody } from '../client/generated/wire.js';
|
|
9
|
+
export interface SlotEditorProps {
|
|
10
|
+
/** The item that owns the upload. */
|
|
11
|
+
item: RefBody;
|
|
12
|
+
/** The upload path, e.g. "cover" or "avatar". */
|
|
13
|
+
path: string;
|
|
14
|
+
/** The public preset showing the upload: children draw it, saves refetch it. */
|
|
15
|
+
image?: PublicPreset | null;
|
|
16
|
+
client?: ContentKitClient;
|
|
17
|
+
/** An editor read of the item from the host; otherwise fetched. */
|
|
18
|
+
read?: ReadResult | null;
|
|
19
|
+
/** Called with the processed upload after every save, null after a removal. */
|
|
20
|
+
onChange?: (file: FileInfo | null) => void;
|
|
21
|
+
/** Every failure (load, decode, save or render); default the provider's. */
|
|
22
|
+
onError?: ContentKitErrorHandler;
|
|
23
|
+
/** The output's "W:H"; default the preset's aspect, else "1:1". */
|
|
24
|
+
aspect?: AspectRatio;
|
|
25
|
+
/** The narrowest edit the server accepts (the preset's Image.MinWidth). */
|
|
26
|
+
minWidth?: number;
|
|
27
|
+
/** "reject" refuses animated images before uploading (the preset's Image.Animation). */
|
|
28
|
+
animation?: "reject";
|
|
29
|
+
/** Crops narrower than this many source pixels get a sharpness warning; default the preset's widest, else 512. */
|
|
30
|
+
targetWidth?: number;
|
|
31
|
+
/** Round crop mask; default aspect 1. */
|
|
32
|
+
round?: boolean;
|
|
33
|
+
/** Crop dialog title; default by aspect (avatar / cover). */
|
|
34
|
+
title?: ReactNode;
|
|
35
|
+
/** `accept` of the file input. Default "image/*". */
|
|
36
|
+
accept?: string;
|
|
37
|
+
disabled?: boolean;
|
|
38
|
+
/** Offers Remove (a remove op) once the path has an upload; default false. */
|
|
39
|
+
removable?: boolean;
|
|
40
|
+
/** Replaces decodeImage (tests). */
|
|
41
|
+
decode?: (file: File) => Promise<CropSource>;
|
|
42
|
+
/** Triggers and the image, e.g. `<SlotEditMenu />`, or anything using useSlotEditor(). */
|
|
43
|
+
children?: ReactNode;
|
|
44
|
+
}
|
|
45
|
+
export interface SlotEditorState {
|
|
46
|
+
image: UseSlotImage;
|
|
47
|
+
crop: UseSlotCrop;
|
|
48
|
+
/** The path has an upload. */
|
|
49
|
+
has: boolean;
|
|
50
|
+
busy: boolean;
|
|
51
|
+
disabled: boolean;
|
|
52
|
+
/** A failure outside the dialog (unreadable file, read failure), localized. */
|
|
53
|
+
error?: string;
|
|
54
|
+
/** Opens the file picker. */
|
|
55
|
+
choose: () => void;
|
|
56
|
+
/** Opens a file for cropping (drop targets). */
|
|
57
|
+
pick: (file: File) => void;
|
|
58
|
+
/** Re-crops the kept upload; only when crop.canRecrop. */
|
|
59
|
+
recrop: () => void;
|
|
60
|
+
/** Remove is offered (SlotEditorProps.removable). */
|
|
61
|
+
removable: boolean;
|
|
62
|
+
/** Removes the upload; only when removable. */
|
|
63
|
+
remove: () => void;
|
|
64
|
+
}
|
|
65
|
+
/** The editor state of the enclosing SlotEditor. */
|
|
66
|
+
export declare function useSlotEditor(): SlotEditorState;
|
|
67
|
+
/**
|
|
68
|
+
* An image upload's pick → crop → save and re-crop flow without layout: it renders a
|
|
69
|
+
* hidden file input and the crop dialog, and its children draw the image and
|
|
70
|
+
* triggers (SlotEditMenu, or custom ones through useSlotEditor).
|
|
71
|
+
*/
|
|
72
|
+
export declare function SlotEditor(p: SlotEditorProps): import('react').JSX.Element;
|
|
73
|
+
export interface SlotEditMenuProps {
|
|
74
|
+
/** Accessible name and text; default "Change" (or "Upload" when empty). */
|
|
75
|
+
label?: string;
|
|
76
|
+
/** Shows only the icon; the label becomes its aria-label. */
|
|
77
|
+
iconOnly?: boolean;
|
|
78
|
+
/**
|
|
79
|
+
* The trigger element (Base UI `render`), e.g. `<button className="…" />`
|
|
80
|
+
* styled by the host; it receives the icon and label unless it has children.
|
|
81
|
+
* Default: the kit's outline button.
|
|
82
|
+
*/
|
|
83
|
+
render?: ReactElement<Record<string, unknown>>;
|
|
84
|
+
className?: string;
|
|
85
|
+
align?: "start" | "center" | "end";
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* One trigger for a SlotEditor: it picks a file, or, once the path has an
|
|
89
|
+
* upload, opens a Change / Edit crop / Remove menu (each when available).
|
|
90
|
+
*/
|
|
91
|
+
export declare function SlotEditMenu({ label, iconOnly, render, className, align }: SlotEditMenuProps): import('react').JSX.Element;
|
|
92
|
+
/** The SlotEditor's error outside the dialog (unreadable file, read failure), if any. */
|
|
93
|
+
export declare function SlotEditError({ className }: {
|
|
94
|
+
className?: string;
|
|
95
|
+
}): import('react').JSX.Element | null;
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
import { toContentKitError as e } from "../client/errors.js";
|
|
2
|
+
import { ratio as t } from "../client/aspect.js";
|
|
3
|
+
import { useMessages as n } from "../i18n/context.js";
|
|
4
|
+
import { useScopeProps as r } from "../scope.js";
|
|
5
|
+
import { useContentKitClient as i, useErrorReporter as a } from "../react/context.js";
|
|
6
|
+
import { useSlotCrop as o, useSlotImage as s } from "../react/slot.js";
|
|
7
|
+
import { Button as c } from "../ui/button.js";
|
|
8
|
+
import { ImageCropDialog as l } from "./image-crop-dialog.js";
|
|
9
|
+
import { DropdownMenu as u, DropdownMenuContent as d, DropdownMenuItem as f, DropdownMenuTrigger as p } from "../ui/dropdown-menu.js";
|
|
10
|
+
import { createContext as m, useContext as h, useEffect as g, useRef as _, useState as v } from "react";
|
|
11
|
+
import { Fragment as y, jsx as b, jsxs as x } from "react/jsx-runtime";
|
|
12
|
+
import { cn as S } from "cn";
|
|
13
|
+
import { Alert02Icon as C, Camera01Icon as w, CropIcon as T, Delete02Icon as E, ImageUpload01Icon as D, Loading03Icon as O } from "@hugeicons/core-free-icons";
|
|
14
|
+
import { HugeiconsIcon as k } from "@hugeicons/react";
|
|
15
|
+
import { Button as A } from "@base-ui/react/button";
|
|
16
|
+
//#region src/components/slot-editor.tsx
|
|
17
|
+
var j = m(null);
|
|
18
|
+
function M() {
|
|
19
|
+
let e = h(j);
|
|
20
|
+
if (!e) throw Error("useSlotEditor must be used inside <SlotEditor>");
|
|
21
|
+
return e;
|
|
22
|
+
}
|
|
23
|
+
function N(r) {
|
|
24
|
+
let { t: c, error: u } = n(), d = i(r.client), f = a(r.onError), p = s({
|
|
25
|
+
client: d,
|
|
26
|
+
ref: r.item,
|
|
27
|
+
path: r.path,
|
|
28
|
+
image: r.image,
|
|
29
|
+
read: r.read
|
|
30
|
+
});
|
|
31
|
+
g(() => void (p.error && f(p.error, "slot.load")), [p.error, f]);
|
|
32
|
+
let m = r.aspect ?? p.aspect, h = _(r.onChange);
|
|
33
|
+
h.current = r.onChange;
|
|
34
|
+
let y = o({
|
|
35
|
+
client: d,
|
|
36
|
+
ref: r.item,
|
|
37
|
+
path: r.path,
|
|
38
|
+
file: p.file,
|
|
39
|
+
aspect: m,
|
|
40
|
+
animation: r.animation,
|
|
41
|
+
decode: r.decode,
|
|
42
|
+
onError: f,
|
|
43
|
+
onSaved: (e) => {
|
|
44
|
+
p.set(e), p.reload(), h.current?.(e);
|
|
45
|
+
}
|
|
46
|
+
}), S = _(null), [C, w] = v(!1), [T, E] = v(), D = y.status === "decoding" || y.status === "saving" || C, O = !!r.disabled || D, k = r.round ?? t(m) === 1, A = r.targetWidth ?? p.renditions.at(-1)?.w ?? 512, M = y.status === "error" && !y.source ? u(y.error) : p.error ? u(p.error) : T ? u(T) : void 0, N = async () => {
|
|
47
|
+
w(!0), E(void 0);
|
|
48
|
+
try {
|
|
49
|
+
p.file && await d.media.commit(r.item, [{
|
|
50
|
+
op: "remove",
|
|
51
|
+
path: p.file.path
|
|
52
|
+
}]), p.set(null), p.reload(), h.current?.(null);
|
|
53
|
+
} catch (t) {
|
|
54
|
+
E(e(t)), f(t, "slot.remove");
|
|
55
|
+
} finally {
|
|
56
|
+
w(!1);
|
|
57
|
+
}
|
|
58
|
+
}, P = {
|
|
59
|
+
image: p,
|
|
60
|
+
crop: y,
|
|
61
|
+
has: !!p.file,
|
|
62
|
+
busy: D,
|
|
63
|
+
disabled: O,
|
|
64
|
+
error: M,
|
|
65
|
+
choose: () => !O && S.current?.click(),
|
|
66
|
+
pick: (e) => !O && void y.pick(e),
|
|
67
|
+
recrop: () => !O && void y.recrop(),
|
|
68
|
+
removable: !!r.removable,
|
|
69
|
+
remove: () => !O && !!r.removable && void N()
|
|
70
|
+
};
|
|
71
|
+
return /* @__PURE__ */ x(j.Provider, {
|
|
72
|
+
value: P,
|
|
73
|
+
children: [
|
|
74
|
+
r.children,
|
|
75
|
+
/* @__PURE__ */ b("input", {
|
|
76
|
+
ref: S,
|
|
77
|
+
type: "file",
|
|
78
|
+
accept: r.accept ?? "image/*",
|
|
79
|
+
hidden: !0,
|
|
80
|
+
"data-ckui": "file",
|
|
81
|
+
onChange: (e) => {
|
|
82
|
+
let t = e.target.files?.[0];
|
|
83
|
+
e.target.value = "", t && y.pick(t);
|
|
84
|
+
}
|
|
85
|
+
}),
|
|
86
|
+
/* @__PURE__ */ b(l, {
|
|
87
|
+
open: "source" in y && !!y.source,
|
|
88
|
+
onOpenChange: (e) => !e && y.cancel(),
|
|
89
|
+
source: "source" in y ? y.source ?? null : null,
|
|
90
|
+
aspect: m,
|
|
91
|
+
round: k,
|
|
92
|
+
initialEdit: "edit" in y && y.mode === "recrop" ? y.edit : void 0,
|
|
93
|
+
targetWidth: A,
|
|
94
|
+
minWidth: r.minWidth,
|
|
95
|
+
title: r.title ?? c(k ? "crop.avatarTitle" : "crop.coverTitle"),
|
|
96
|
+
busy: y.status === "saving",
|
|
97
|
+
progress: y.status === "saving" ? y.progress : void 0,
|
|
98
|
+
rendering: y.status === "saving" && y.rendering,
|
|
99
|
+
error: y.status === "error" && y.source ? u(y.error) : void 0,
|
|
100
|
+
onEditChange: y.setEdit,
|
|
101
|
+
onConfirm: (e) => void y.save(e)
|
|
102
|
+
})
|
|
103
|
+
]
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
function P({ label: e, iconOnly: t, render: i, className: a, align: o = "end" }) {
|
|
107
|
+
let { t: s } = n(), l = M(), m = r(), h = e ?? (l.has ? s("common.change") : s("common.upload")), g = /* @__PURE__ */ x(y, { children: [/* @__PURE__ */ b(k, {
|
|
108
|
+
icon: l.busy ? O : w,
|
|
109
|
+
size: 16,
|
|
110
|
+
strokeWidth: 2,
|
|
111
|
+
"data-icon": "inline-start",
|
|
112
|
+
className: l.busy ? "animate-spin" : void 0
|
|
113
|
+
}), !t && h] }), _ = {
|
|
114
|
+
render: i ?? /* @__PURE__ */ b(c, {
|
|
115
|
+
variant: "outline",
|
|
116
|
+
size: t ? "icon-sm" : "sm",
|
|
117
|
+
...m,
|
|
118
|
+
className: S(m.className, a),
|
|
119
|
+
style: m.style
|
|
120
|
+
}),
|
|
121
|
+
disabled: l.disabled,
|
|
122
|
+
title: h,
|
|
123
|
+
"aria-label": t ? h : void 0,
|
|
124
|
+
"data-ckui": "slot-edit",
|
|
125
|
+
"data-busy": l.busy || void 0,
|
|
126
|
+
className: i ? a : void 0
|
|
127
|
+
};
|
|
128
|
+
return l.has && (l.crop.canRecrop || l.removable) ? /* @__PURE__ */ x(u, { children: [/* @__PURE__ */ b(p, {
|
|
129
|
+
..._,
|
|
130
|
+
children: g
|
|
131
|
+
}), /* @__PURE__ */ x(d, {
|
|
132
|
+
align: o,
|
|
133
|
+
children: [
|
|
134
|
+
/* @__PURE__ */ x(f, {
|
|
135
|
+
onClick: l.choose,
|
|
136
|
+
"data-ckui": "change",
|
|
137
|
+
children: [/* @__PURE__ */ b(k, {
|
|
138
|
+
icon: D,
|
|
139
|
+
strokeWidth: 2
|
|
140
|
+
}), s("common.change")]
|
|
141
|
+
}),
|
|
142
|
+
l.crop.canRecrop && /* @__PURE__ */ x(f, {
|
|
143
|
+
onClick: l.recrop,
|
|
144
|
+
"data-ckui": "edit-crop",
|
|
145
|
+
children: [/* @__PURE__ */ b(k, {
|
|
146
|
+
icon: T,
|
|
147
|
+
strokeWidth: 2
|
|
148
|
+
}), s("common.editCrop")]
|
|
149
|
+
}),
|
|
150
|
+
l.removable && /* @__PURE__ */ x(f, {
|
|
151
|
+
onClick: l.remove,
|
|
152
|
+
"data-ckui": "remove",
|
|
153
|
+
children: [/* @__PURE__ */ b(k, {
|
|
154
|
+
icon: E,
|
|
155
|
+
strokeWidth: 2
|
|
156
|
+
}), s("common.remove")]
|
|
157
|
+
})
|
|
158
|
+
]
|
|
159
|
+
})] }) : /* @__PURE__ */ b(A, {
|
|
160
|
+
..._,
|
|
161
|
+
onClick: l.choose,
|
|
162
|
+
children: g
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
function F({ className: e }) {
|
|
166
|
+
let { error: t } = M(), n = r();
|
|
167
|
+
return t ? /* @__PURE__ */ x("p", {
|
|
168
|
+
role: "alert",
|
|
169
|
+
"data-ckui": "slot-error",
|
|
170
|
+
...n,
|
|
171
|
+
className: S(n.className, "flex items-center gap-1.5 text-xs text-destructive", e),
|
|
172
|
+
children: [/* @__PURE__ */ b(k, {
|
|
173
|
+
icon: C,
|
|
174
|
+
strokeWidth: 2,
|
|
175
|
+
className: "size-3.5 shrink-0"
|
|
176
|
+
}), t]
|
|
177
|
+
}) : null;
|
|
178
|
+
}
|
|
179
|
+
//#endregion
|
|
180
|
+
export { F as SlotEditError, P as SlotEditMenu, N as SlotEditor, M as useSlotEditor };
|
|
181
|
+
|
|
182
|
+
//# sourceMappingURL=slot-editor.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"slot-editor.js","names":[],"sources":["../../src/components/slot-editor.tsx"],"sourcesContent":["import { ratio, type AspectRatio } from \"../client/aspect.js\";\nimport { Alert02Icon, Camera01Icon, CropIcon, Delete02Icon, ImageUpload01Icon, Loading03Icon } from \"@hugeicons/core-free-icons\";\nimport { Button as ButtonPrimitive } from \"@base-ui/react/button\";\nimport { HugeiconsIcon } from \"@hugeicons/react\";\nimport { cn } from \"cn\";\nimport { createContext, useContext, useEffect, useRef, useState, type ReactElement, type ReactNode } from \"react\";\nimport type { ContentKitClient } from \"../client/client.js\";\nimport { useMessages } from \"../i18n/context.js\";\nimport type { CropSource } from \"../client/image.js\";\nimport { toContentKitError, type ContentKitError } from \"../client/errors.js\";\nimport { useContentKitClient, useErrorReporter, type ContentKitErrorHandler } from \"../react/context.js\";\nimport { useScopeProps } from \"../scope.js\";\nimport type { PublicPreset } from \"../client/public.js\";\nimport { useSlotCrop, useSlotImage, type UseSlotCrop, type UseSlotImage } from \"../react/slot.js\";\nimport type { FileInfo, ReadResult, RefBody } from \"../client/generated/wire.js\";\nimport { Button } from \"#ckui/ui/button\";\nimport { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from \"#ckui/ui/dropdown-menu\";\nimport { ImageCropDialog } from \"./image-crop-dialog.js\";\n\nexport interface SlotEditorProps {\n /** The item that owns the upload. */\n item: RefBody;\n /** The upload path, e.g. \"cover\" or \"avatar\". */\n path: string;\n /** The public preset showing the upload: children draw it, saves refetch it. */\n image?: PublicPreset | null;\n client?: ContentKitClient;\n /** An editor read of the item from the host; otherwise fetched. */\n read?: ReadResult | null;\n /** Called with the processed upload after every save, null after a removal. */\n onChange?: (file: FileInfo | null) => void;\n /** Every failure (load, decode, save or render); default the provider's. */\n onError?: ContentKitErrorHandler;\n /** The output's \"W:H\"; default the preset's aspect, else \"1:1\". */\n aspect?: AspectRatio;\n /** The narrowest edit the server accepts (the preset's Image.MinWidth). */\n minWidth?: number;\n /** \"reject\" refuses animated images before uploading (the preset's Image.Animation). */\n animation?: \"reject\";\n /** Crops narrower than this many source pixels get a sharpness warning; default the preset's widest, else 512. */\n targetWidth?: number;\n /** Round crop mask; default aspect 1. */\n round?: boolean;\n /** Crop dialog title; default by aspect (avatar / cover). */\n title?: ReactNode;\n /** `accept` of the file input. Default \"image/*\". */\n accept?: string;\n disabled?: boolean;\n /** Offers Remove (a remove op) once the path has an upload; default false. */\n removable?: boolean;\n /** Replaces decodeImage (tests). */\n decode?: (file: File) => Promise<CropSource>;\n /** Triggers and the image, e.g. `<SlotEditMenu />`, or anything using useSlotEditor(). */\n children?: ReactNode;\n}\n\nexport interface SlotEditorState {\n image: UseSlotImage;\n crop: UseSlotCrop;\n /** The path has an upload. */\n has: boolean;\n busy: boolean;\n disabled: boolean;\n /** A failure outside the dialog (unreadable file, read failure), localized. */\n error?: string;\n /** Opens the file picker. */\n choose: () => void;\n /** Opens a file for cropping (drop targets). */\n pick: (file: File) => void;\n /** Re-crops the kept upload; only when crop.canRecrop. */\n recrop: () => void;\n /** Remove is offered (SlotEditorProps.removable). */\n removable: boolean;\n /** Removes the upload; only when removable. */\n remove: () => void;\n}\n\nconst Ctx = createContext<SlotEditorState | null>(null);\n\n/** The editor state of the enclosing SlotEditor. */\nexport function useSlotEditor(): SlotEditorState {\n const s = useContext(Ctx);\n if (!s) throw new Error(\"useSlotEditor must be used inside <SlotEditor>\");\n return s;\n}\n\n/**\n * An image upload's pick → crop → save and re-crop flow without layout: it renders a\n * hidden file input and the crop dialog, and its children draw the image and\n * triggers (SlotEditMenu, or custom ones through useSlotEditor).\n */\nexport function SlotEditor(p: SlotEditorProps) {\n const { t, error: errorText } = useMessages();\n const client = useContentKitClient(p.client);\n const report = useErrorReporter(p.onError);\n const image = useSlotImage({ client, ref: p.item, path: p.path, image: p.image, read: p.read });\n useEffect(() => void (image.error && report(image.error, \"slot.load\")), [image.error, report]);\n const aspect = p.aspect ?? image.aspect;\n const onChange = useRef(p.onChange);\n onChange.current = p.onChange;\n const crop = useSlotCrop({\n client,\n ref: p.item,\n path: p.path,\n file: image.file,\n aspect,\n animation: p.animation,\n decode: p.decode,\n onError: report,\n onSaved: (f) => {\n image.set(f);\n image.reload();\n onChange.current?.(f);\n },\n });\n const input = useRef<HTMLInputElement>(null);\n const [removing, setRemoving] = useState(false);\n const [removeError, setRemoveError] = useState<ContentKitError>();\n const busy = crop.status === \"decoding\" || crop.status === \"saving\" || removing;\n const disabled = !!p.disabled || busy;\n const round = p.round ?? ratio(aspect) === 1;\n const target = p.targetWidth ?? image.renditions.at(-1)?.w ?? 512;\n const error =\n crop.status === \"error\" && !crop.source\n ? errorText(crop.error)\n : image.error\n ? errorText(image.error)\n : removeError\n ? errorText(removeError)\n : undefined;\n const remove = async () => {\n setRemoving(true);\n setRemoveError(undefined);\n try {\n if (image.file) await client.media.commit(p.item, [{ op: \"remove\", path: image.file.path }]);\n image.set(null);\n image.reload();\n onChange.current?.(null);\n } catch (e) {\n setRemoveError(toContentKitError(e));\n report(e, \"slot.remove\");\n } finally {\n setRemoving(false);\n }\n };\n const state: SlotEditorState = {\n image,\n crop,\n has: !!image.file,\n busy,\n disabled,\n error,\n choose: () => !disabled && input.current?.click(),\n pick: (f) => !disabled && void crop.pick(f),\n recrop: () => !disabled && void crop.recrop(),\n removable: !!p.removable,\n remove: () => !disabled && !!p.removable && void remove(),\n };\n return (\n <Ctx.Provider value={state}>\n {p.children}\n <input\n ref={input}\n type=\"file\"\n accept={p.accept ?? \"image/*\"}\n hidden\n data-ckui=\"file\"\n onChange={(e) => {\n const f = e.target.files?.[0];\n e.target.value = \"\";\n if (f) void crop.pick(f);\n }}\n />\n <ImageCropDialog\n open={\"source\" in crop && !!crop.source}\n onOpenChange={(o) => !o && crop.cancel()}\n source={\"source\" in crop ? (crop.source ?? null) : null}\n aspect={aspect}\n round={round}\n initialEdit={\"edit\" in crop && crop.mode === \"recrop\" ? crop.edit : undefined}\n targetWidth={target}\n minWidth={p.minWidth}\n title={p.title ?? t(round ? \"crop.avatarTitle\" : \"crop.coverTitle\")}\n busy={crop.status === \"saving\"}\n progress={crop.status === \"saving\" ? crop.progress : undefined}\n rendering={crop.status === \"saving\" && crop.rendering}\n error={crop.status === \"error\" && crop.source ? errorText(crop.error) : undefined}\n onEditChange={crop.setEdit}\n onConfirm={(e) => void crop.save(e)}\n />\n </Ctx.Provider>\n );\n}\n\nexport interface SlotEditMenuProps {\n /** Accessible name and text; default \"Change\" (or \"Upload\" when empty). */\n label?: string;\n /** Shows only the icon; the label becomes its aria-label. */\n iconOnly?: boolean;\n /**\n * The trigger element (Base UI `render`), e.g. `<button className=\"…\" />`\n * styled by the host; it receives the icon and label unless it has children.\n * Default: the kit's outline button.\n */\n render?: ReactElement<Record<string, unknown>>;\n className?: string;\n align?: \"start\" | \"center\" | \"end\";\n}\n\n/**\n * One trigger for a SlotEditor: it picks a file, or, once the path has an\n * upload, opens a Change / Edit crop / Remove menu (each when available).\n */\nexport function SlotEditMenu({ label, iconOnly, render, className, align = \"end\" }: SlotEditMenuProps) {\n const { t } = useMessages();\n const s = useSlotEditor();\n const scope = useScopeProps();\n const name = label ?? (s.has ? t(\"common.change\") : t(\"common.upload\"));\n const content = (\n <>\n <HugeiconsIcon icon={s.busy ? Loading03Icon : Camera01Icon} size={16} strokeWidth={2} data-icon=\"inline-start\" className={s.busy ? \"animate-spin\" : undefined} />\n {!iconOnly && name}\n </>\n );\n // The kit's button carries the style scope itself: no wrapper to break host layouts.\n const trigger = render ?? <Button variant=\"outline\" size={iconOnly ? \"icon-sm\" : \"sm\"} {...scope} className={cn(scope.className, className)} style={scope.style} />;\n const props = {\n render: trigger,\n disabled: s.disabled,\n title: name,\n \"aria-label\": iconOnly ? name : undefined,\n \"data-ckui\": \"slot-edit\",\n \"data-busy\": s.busy || undefined,\n className: render ? className : undefined,\n };\n if (!(s.has && (s.crop.canRecrop || s.removable))) {\n return (\n <ButtonPrimitive {...props} onClick={s.choose}>\n {content}\n </ButtonPrimitive>\n );\n }\n return (\n <DropdownMenu>\n <DropdownMenuTrigger {...props}>{content}</DropdownMenuTrigger>\n <DropdownMenuContent align={align}>\n <DropdownMenuItem onClick={s.choose} data-ckui=\"change\">\n <HugeiconsIcon icon={ImageUpload01Icon} strokeWidth={2} />\n {t(\"common.change\")}\n </DropdownMenuItem>\n {s.crop.canRecrop && (\n <DropdownMenuItem onClick={s.recrop} data-ckui=\"edit-crop\">\n <HugeiconsIcon icon={CropIcon} strokeWidth={2} />\n {t(\"common.editCrop\")}\n </DropdownMenuItem>\n )}\n {s.removable && (\n <DropdownMenuItem onClick={s.remove} data-ckui=\"remove\">\n <HugeiconsIcon icon={Delete02Icon} strokeWidth={2} />\n {t(\"common.remove\")}\n </DropdownMenuItem>\n )}\n </DropdownMenuContent>\n </DropdownMenu>\n );\n}\n\n/** The SlotEditor's error outside the dialog (unreadable file, read failure), if any. */\nexport function SlotEditError({ className }: { className?: string }) {\n const { error } = useSlotEditor();\n const scope = useScopeProps();\n if (!error) return null;\n return (\n <p role=\"alert\" data-ckui=\"slot-error\" {...scope} className={cn(scope.className, \"flex items-center gap-1.5 text-xs text-destructive\", className)}>\n <HugeiconsIcon icon={Alert02Icon} strokeWidth={2} className=\"size-3.5 shrink-0\" />\n {error}\n </p>\n );\n}\n"],"mappings":";;;;;;;;;;;;;;;;AA6EA,IAAM,IAAM,EAAsC,IAAI;AAGtD,SAAgB,IAAiC;CAC/C,IAAM,IAAI,EAAW,CAAG;CACxB,IAAI,CAAC,GAAG,MAAU,MAAM,gDAAgD;CACxE,OAAO;AACT;AAOA,SAAgB,EAAW,GAAoB;CAC7C,IAAM,EAAE,MAAG,OAAO,MAAc,EAAY,GACtC,IAAS,EAAoB,EAAE,MAAM,GACrC,IAAS,EAAiB,EAAE,OAAO,GACnC,IAAQ,EAAa;EAAE;EAAQ,KAAK,EAAE;EAAM,MAAM,EAAE;EAAM,OAAO,EAAE;EAAO,MAAM,EAAE;CAAK,CAAC;CAC9F,QAAgB,MAAM,EAAM,SAAS,EAAO,EAAM,OAAO,WAAW,IAAI,CAAC,EAAM,OAAO,CAAM,CAAC;CAC7F,IAAM,IAAS,EAAE,UAAU,EAAM,QAC3B,IAAW,EAAO,EAAE,QAAQ;CAClC,EAAS,UAAU,EAAE;CACrB,IAAM,IAAO,EAAY;EACvB;EACA,KAAK,EAAE;EACP,MAAM,EAAE;EACR,MAAM,EAAM;EACZ;EACA,WAAW,EAAE;EACb,QAAQ,EAAE;EACV,SAAS;EACT,UAAU,MAAM;GAGd,AAFA,EAAM,IAAI,CAAC,GACX,EAAM,OAAO,GACb,EAAS,UAAU,CAAC;EACtB;CACF,CAAC,GACK,IAAQ,EAAyB,IAAI,GACrC,CAAC,GAAU,KAAe,EAAS,EAAK,GACxC,CAAC,GAAa,KAAkB,EAA0B,GAC1D,IAAO,EAAK,WAAW,cAAc,EAAK,WAAW,YAAY,GACjE,IAAW,CAAC,CAAC,EAAE,YAAY,GAC3B,IAAQ,EAAE,SAAS,EAAM,CAAM,MAAM,GACrC,IAAS,EAAE,eAAe,EAAM,WAAW,GAAG,EAAE,CAAC,EAAE,KAAK,KACxD,IACJ,EAAK,WAAW,WAAW,CAAC,EAAK,SAC7B,EAAU,EAAK,KAAK,IACpB,EAAM,QACJ,EAAU,EAAM,KAAK,IACrB,IACE,EAAU,CAAW,IACrB,KAAA,GACJ,IAAS,YAAY;EAEzB,AADA,EAAY,EAAI,GAChB,EAAe,KAAA,CAAS;EACxB,IAAI;GAIF,AAHI,EAAM,QAAM,MAAM,EAAO,MAAM,OAAO,EAAE,MAAM,CAAC;IAAE,IAAI;IAAU,MAAM,EAAM,KAAK;GAAK,CAAC,CAAC,GAC3F,EAAM,IAAI,IAAI,GACd,EAAM,OAAO,GACb,EAAS,UAAU,IAAI;EACzB,SAAS,GAAG;GAEV,AADA,EAAe,EAAkB,CAAC,CAAC,GACnC,EAAO,GAAG,aAAa;EACzB,UAAU;GACR,EAAY,EAAK;EACnB;CACF,GACM,IAAyB;EAC7B;EACA;EACA,KAAK,CAAC,CAAC,EAAM;EACb;EACA;EACA;EACA,cAAc,CAAC,KAAY,EAAM,SAAS,MAAM;EAChD,OAAO,MAAM,CAAC,KAAY,KAAK,EAAK,KAAK,CAAC;EAC1C,cAAc,CAAC,KAAY,KAAK,EAAK,OAAO;EAC5C,WAAW,CAAC,CAAC,EAAE;EACf,cAAc,CAAC,KAAY,CAAC,CAAC,EAAE,aAAa,KAAK,EAAO;CAC1D;CACA,OACE,kBAAC,EAAI,UAAL;EAAc,OAAO;EAArB,UAAA;GACG,EAAE;GACH,kBAAC,SAAD;IACE,KAAK;IACL,MAAK;IACL,QAAQ,EAAE,UAAU;IACpB,QAAA;IACA,aAAU;IACV,WAAW,MAAM;KACf,IAAM,IAAI,EAAE,OAAO,QAAQ;KAE3B,AADA,EAAE,OAAO,QAAQ,IACb,KAAG,EAAU,KAAK,CAAC;IACzB;GACD,CAAA;GACD,kBAAC,GAAD;IACE,MAAM,YAAY,KAAQ,CAAC,CAAC,EAAK;IACjC,eAAe,MAAM,CAAC,KAAK,EAAK,OAAO;IACvC,QAAQ,YAAY,IAAQ,EAAK,UAAU,OAAQ;IAC3C;IACD;IACP,aAAa,UAAU,KAAQ,EAAK,SAAS,WAAW,EAAK,OAAO,KAAA;IACpE,aAAa;IACb,UAAU,EAAE;IACZ,OAAO,EAAE,SAAS,EAAE,IAAQ,qBAAqB,iBAAiB;IAClE,MAAM,EAAK,WAAW;IACtB,UAAU,EAAK,WAAW,WAAW,EAAK,WAAW,KAAA;IACrD,WAAW,EAAK,WAAW,YAAY,EAAK;IAC5C,OAAO,EAAK,WAAW,WAAW,EAAK,SAAS,EAAU,EAAK,KAAK,IAAI,KAAA;IACxE,cAAc,EAAK;IACnB,YAAY,MAAM,KAAK,EAAK,KAAK,CAAC;GACnC,CAAA;EACW;;AAElB;AAqBA,SAAgB,EAAa,EAAE,UAAO,aAAU,WAAQ,cAAW,WAAQ,SAA4B;CACrG,IAAM,EAAE,SAAM,EAAY,GACpB,IAAI,EAAc,GAClB,IAAQ,EAAc,GACtB,IAAO,MAAU,EAAE,MAAM,EAAE,eAAe,IAAI,EAAE,eAAe,IAC/D,IACJ,kBAAA,GAAA,EAAA,UAAA,CACE,kBAAC,GAAD;EAAe,MAAM,EAAE,OAAO,IAAgB;EAAc,MAAM;EAAI,aAAa;EAAG,aAAU;EAAe,WAAW,EAAE,OAAO,iBAAiB,KAAA;CAAY,CAAA,GAC/J,CAAC,KAAY,CACd,EAAA,CAAA,GAIE,IAAQ;EACZ,QAFc,KAAU,kBAAC,GAAD;GAAQ,SAAQ;GAAU,MAAM,IAAW,YAAY;GAAM,GAAI;GAAO,WAAW,EAAG,EAAM,WAAW,CAAS;GAAG,OAAO,EAAM;EAAQ,CAAA;EAGhK,UAAU,EAAE;EACZ,OAAO;EACP,cAAc,IAAW,IAAO,KAAA;EAChC,aAAa;EACb,aAAa,EAAE,QAAQ,KAAA;EACvB,WAAW,IAAS,IAAY,KAAA;CAClC;CAQA,OAPM,EAAE,QAAQ,EAAE,KAAK,aAAa,EAAE,aAQpC,kBAAC,GAAD,EAAA,UAAA,CACE,kBAAC,GAAD;EAAqB,GAAI;EAAQ,UAAA;CAA6B,CAAA,GAC9D,kBAAC,GAAD;EAA4B;EAA5B,UAAA;GACE,kBAAC,GAAD;IAAkB,SAAS,EAAE;IAAQ,aAAU;IAA/C,UAAA,CACE,kBAAC,GAAD;KAAe,MAAM;KAAmB,aAAa;IAAI,CAAA,GACxD,EAAE,eAAe,CACF;;GACjB,EAAE,KAAK,aACN,kBAAC,GAAD;IAAkB,SAAS,EAAE;IAAQ,aAAU;IAA/C,UAAA,CACE,kBAAC,GAAD;KAAe,MAAM;KAAU,aAAa;IAAI,CAAA,GAC/C,EAAE,iBAAiB,CACJ;;GAEnB,EAAE,aACD,kBAAC,GAAD;IAAkB,SAAS,EAAE;IAAQ,aAAU;IAA/C,UAAA,CACE,kBAAC,GAAD;KAAe,MAAM;KAAc,aAAa;IAAI,CAAA,GACnD,EAAE,eAAe,CACF;;EAED;CACT,CAAA,CAAA,EAAA,CAAA,IA1BZ,kBAAC,GAAD;EAAiB,GAAI;EAAO,SAAS,EAAE;EACpC,UAAA;CACc,CAAA;AA0BvB;AAGA,SAAgB,EAAc,EAAE,gBAAqC;CACnE,IAAM,EAAE,aAAU,EAAc,GAC1B,IAAQ,EAAc;CAE5B,OADK,IAEH,kBAAC,KAAD;EAAG,MAAK;EAAQ,aAAU;EAAa,GAAI;EAAO,WAAW,EAAG,EAAM,WAAW,sDAAsD,CAAS;EAAhJ,UAAA,CACE,kBAAC,GAAD;GAAe,MAAM;GAAa,aAAa;GAAG,WAAU;EAAqB,CAAA,GAChF,CACA;MALc;AAOrB"}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { AspectRatio } from '../client/aspect.js';
|
|
2
|
+
import { ComponentProps, ReactNode } from 'react';
|
|
3
|
+
import { PublicPreset } from '../client/public.js';
|
|
4
|
+
import { DensityRange } from '../client/rendition.js';
|
|
5
|
+
export interface SlotImageProps extends Omit<ComponentProps<"img">, "src" | "srcSet" | "sizes" | "width" | "height"> {
|
|
6
|
+
/** The item's published renditions; without any, the fallback shows. */
|
|
7
|
+
image?: PublicPreset | null;
|
|
8
|
+
/** Density range for picking the width; default the provider's (2–3×). */
|
|
9
|
+
density?: DensityRange;
|
|
10
|
+
round?: boolean;
|
|
11
|
+
/** Width / height of the box; default the preset's aspect. */
|
|
12
|
+
aspect?: AspectRatio;
|
|
13
|
+
/** Shown without an image (or when it fails to load); default a muted box with an icon. */
|
|
14
|
+
fallback?: ReactNode;
|
|
15
|
+
/** A pulsing fill until the image has loaded. */
|
|
16
|
+
skeleton?: boolean;
|
|
17
|
+
/** How the image fills the box. Default "cover". */
|
|
18
|
+
fit?: "cover" | "contain";
|
|
19
|
+
/** Accessible name of the empty state. */
|
|
20
|
+
emptyLabel?: string;
|
|
21
|
+
}
|
|
22
|
+
/** A public image preset's file, in a box at its aspect, at the width its rendered width × density needs. */
|
|
23
|
+
export declare function SlotImage({ image, density, round, aspect, fallback, skeleton, fit, emptyLabel, className, alt, style, onError, ...img }: SlotImageProps): import('react').JSX.Element;
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { RenditionImg as e } from "./rendition-img.js";
|
|
2
|
+
import { publicRenditions as t } from "../client/public.js";
|
|
3
|
+
import { ratio as n } from "../client/aspect.js";
|
|
4
|
+
import { ContentKitUiRoot as r } from "../scope.js";
|
|
5
|
+
import { createElement as i, useState as a } from "react";
|
|
6
|
+
import { jsx as o } from "react/jsx-runtime";
|
|
7
|
+
import { cn as s } from "cn";
|
|
8
|
+
import { Image01Icon as c, UserIcon as l } from "@hugeicons/core-free-icons";
|
|
9
|
+
import { HugeiconsIcon as u } from "@hugeicons/react";
|
|
10
|
+
//#region src/components/slot-image.tsx
|
|
11
|
+
function d({ image: d, density: f, round: p, aspect: m, fallback: h, skeleton: g, fit: _ = "cover", emptyLabel: v, className: y, alt: b = "", style: x, onError: S, ...C }) {
|
|
12
|
+
let w = t(d), [T, E] = a(null), D = w[0]?.url ?? "", O = w.length > 0 && T !== D, k = n(m ?? d?.aspect) ?? 1;
|
|
13
|
+
return /* @__PURE__ */ o(r, {
|
|
14
|
+
className: s("relative overflow-hidden bg-muted", p ? "rounded-full" : "rounded-lg", y),
|
|
15
|
+
style: {
|
|
16
|
+
aspectRatio: String(k),
|
|
17
|
+
...x
|
|
18
|
+
},
|
|
19
|
+
"data-ckui": "slot-image",
|
|
20
|
+
"data-empty": O ? void 0 : "",
|
|
21
|
+
children: O ? /* @__PURE__ */ i(e, {
|
|
22
|
+
...C,
|
|
23
|
+
key: D,
|
|
24
|
+
outputs: w,
|
|
25
|
+
density: f,
|
|
26
|
+
alt: b,
|
|
27
|
+
skeleton: g,
|
|
28
|
+
fit: _,
|
|
29
|
+
className: "absolute inset-0 size-full",
|
|
30
|
+
onError: (e) => {
|
|
31
|
+
E(D), S?.(e);
|
|
32
|
+
}
|
|
33
|
+
}) : h ?? /* @__PURE__ */ o("div", {
|
|
34
|
+
role: v ? "img" : void 0,
|
|
35
|
+
"aria-label": v,
|
|
36
|
+
className: "absolute inset-0 flex items-center justify-center text-muted-foreground/70",
|
|
37
|
+
children: /* @__PURE__ */ o(u, {
|
|
38
|
+
icon: p ? l : c,
|
|
39
|
+
strokeWidth: 1.5,
|
|
40
|
+
className: "size-1/3 max-h-10 max-w-10"
|
|
41
|
+
})
|
|
42
|
+
})
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
//#endregion
|
|
46
|
+
export { d as SlotImage };
|
|
47
|
+
|
|
48
|
+
//# sourceMappingURL=slot-image.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"slot-image.js","names":[],"sources":["../../src/components/slot-image.tsx"],"sourcesContent":["import { ratio, type AspectRatio } from \"../client/aspect.js\";\nimport { Image01Icon, UserIcon } from \"@hugeicons/core-free-icons\";\nimport { HugeiconsIcon } from \"@hugeicons/react\";\nimport { cn } from \"cn\";\nimport { useState, type ComponentProps, type ReactNode } from \"react\";\nimport { publicRenditions, type PublicPreset } from \"../client/public.js\";\nimport type { DensityRange } from \"../client/rendition.js\";\nimport { RenditionImg } from \"./rendition-img.js\";\nimport { ContentKitUiRoot } from \"../scope.js\";\n\nexport interface SlotImageProps extends Omit<ComponentProps<\"img\">, \"src\" | \"srcSet\" | \"sizes\" | \"width\" | \"height\"> {\n /** The item's published renditions; without any, the fallback shows. */\n image?: PublicPreset | null;\n /** Density range for picking the width; default the provider's (2–3×). */\n density?: DensityRange;\n round?: boolean;\n /** Width / height of the box; default the preset's aspect. */\n aspect?: AspectRatio;\n /** Shown without an image (or when it fails to load); default a muted box with an icon. */\n fallback?: ReactNode;\n /** A pulsing fill until the image has loaded. */\n skeleton?: boolean;\n /** How the image fills the box. Default \"cover\". */\n fit?: \"cover\" | \"contain\";\n /** Accessible name of the empty state. */\n emptyLabel?: string;\n}\n\n/** A public image preset's file, in a box at its aspect, at the width its rendered width × density needs. */\nexport function SlotImage({ image, density, round, aspect, fallback, skeleton, fit = \"cover\", emptyLabel, className, alt = \"\", style, onError, ...img }: SlotImageProps) {\n const outputs = publicRenditions(image);\n const [failed, setFailed] = useState<string | null>(null);\n const key = outputs[0]?.url ?? \"\";\n const has = outputs.length > 0 && failed !== key;\n const a = ratio(aspect ?? image?.aspect) ?? 1;\n return (\n <ContentKitUiRoot\n className={cn(\"relative overflow-hidden bg-muted\", round ? \"rounded-full\" : \"rounded-lg\", className)}\n style={{ aspectRatio: String(a), ...style }}\n data-ckui=\"slot-image\"\n data-empty={has ? undefined : \"\"}\n >\n {has ? (\n <RenditionImg\n {...img}\n key={key}\n outputs={outputs}\n density={density}\n alt={alt}\n skeleton={skeleton}\n fit={fit}\n className=\"absolute inset-0 size-full\"\n onError={(e) => {\n setFailed(key);\n onError?.(e);\n }}\n />\n ) : (\n (fallback ?? (\n <div role={emptyLabel ? \"img\" : undefined} aria-label={emptyLabel} className=\"absolute inset-0 flex items-center justify-center text-muted-foreground/70\">\n <HugeiconsIcon icon={round ? UserIcon : Image01Icon} strokeWidth={1.5} className=\"size-1/3 max-h-10 max-w-10\" />\n </div>\n ))\n )}\n </ContentKitUiRoot>\n );\n}\n"],"mappings":";;;;;;;;;;AA6BA,SAAgB,EAAU,EAAE,UAAO,YAAS,UAAO,WAAQ,aAAU,aAAU,SAAM,SAAS,eAAY,cAAW,SAAM,IAAI,UAAO,YAAS,GAAG,KAAuB;CACvK,IAAM,IAAU,EAAiB,CAAK,GAChC,CAAC,GAAQ,KAAa,EAAwB,IAAI,GAClD,IAAM,EAAQ,EAAE,EAAE,OAAO,IACzB,IAAM,EAAQ,SAAS,KAAK,MAAW,GACvC,IAAI,EAAM,KAAU,GAAO,MAAM,KAAK;CAC5C,OACE,kBAAC,GAAD;EACE,WAAW,EAAG,qCAAqC,IAAQ,iBAAiB,cAAc,CAAS;EACnG,OAAO;GAAE,aAAa,OAAO,CAAC;GAAG,GAAG;EAAM;EAC1C,aAAU;EACV,cAAY,IAAM,KAAA,IAAY;EAE7B,UAAA,IACC,kBAAC,GAAD;GACE,GAAI;GACC;GACI;GACA;GACJ;GACK;GACL;GACL,WAAU;GACV,UAAU,MAAM;IAEd,AADA,EAAU,CAAG,GACb,IAAU,CAAC;GACb;EACD,CAAA,IAEA,KACC,kBAAC,OAAD;GAAK,MAAM,IAAa,QAAQ,KAAA;GAAW,cAAY;GAAY,WAAU;GAC3E,UAAA,kBAAC,GAAD;IAAe,MAAM,IAAQ,IAAW;IAAa,aAAa;IAAK,WAAU;GAA8B,CAAA;EAC5G,CAAA;CAGO,CAAA;AAEtB"}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { AspectRatio } from '../client/aspect.js';
|
|
2
|
+
import { ReactNode } from 'react';
|
|
3
|
+
import { ContentKitClient } from '../client/client.js';
|
|
4
|
+
import { CropSource } from '../client/image.js';
|
|
5
|
+
import { PublicPreset } from '../client/public.js';
|
|
6
|
+
import { FileInfo, ReadResult, RefBody } from '../client/generated/wire.js';
|
|
7
|
+
export interface SlotUploadProps {
|
|
8
|
+
/** The item that owns the upload. */
|
|
9
|
+
item: RefBody;
|
|
10
|
+
/** The upload path; default "avatar" or "cover". */
|
|
11
|
+
path?: string;
|
|
12
|
+
/** The public preset showing it. */
|
|
13
|
+
image?: PublicPreset | null;
|
|
14
|
+
client?: ContentKitClient;
|
|
15
|
+
/** An editor read of the item from the host; otherwise fetched. */
|
|
16
|
+
read?: ReadResult | null;
|
|
17
|
+
/** Called with the processed upload after every save, null after a removal. */
|
|
18
|
+
onChange?: (file: FileInfo | null) => void;
|
|
19
|
+
/** The output's "W:H"; default the preset's aspect, else "1:1" (avatar) or "3:1" (cover). */
|
|
20
|
+
aspect?: AspectRatio;
|
|
21
|
+
/** The narrowest edit the server accepts (the preset's Image.MinWidth). */
|
|
22
|
+
minWidth?: number;
|
|
23
|
+
/** "reject" refuses animated images before uploading. */
|
|
24
|
+
animation?: "reject";
|
|
25
|
+
/** Crops narrower than this many source pixels get a sharpness warning; default 512 (avatar) or 3000 (cover). */
|
|
26
|
+
targetWidth?: number;
|
|
27
|
+
/** `accept` of the file input. Default "image/*". */
|
|
28
|
+
accept?: string;
|
|
29
|
+
disabled?: boolean;
|
|
30
|
+
/** Offers Remove once set; default true for avatars, false for covers. */
|
|
31
|
+
removable?: boolean;
|
|
32
|
+
label?: ReactNode;
|
|
33
|
+
hint?: ReactNode;
|
|
34
|
+
className?: string;
|
|
35
|
+
/** Replaces decodeImage (tests). */
|
|
36
|
+
decode?: (file: File) => Promise<CropSource>;
|
|
37
|
+
}
|
|
38
|
+
/** Round 1:1 avatar field: image, label, hint, Change / Edit crop, crop dialog, progress, errors. For custom layouts use SlotEditor. */
|
|
39
|
+
export declare function AvatarUpload(p: SlotUploadProps): import('react').JSX.Element;
|
|
40
|
+
/** Wide cover field at the preset's aspect (e.g. 3:1), with drop-to-upload. For custom layouts use SlotEditor. */
|
|
41
|
+
export declare function CoverUpload(p: SlotUploadProps): import('react').JSX.Element;
|