@hoardodile/ui 0.1.9 → 0.1.11
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{chunk-2BVZEYNW.js → chunk-6JMORBVL.js} +34 -11
- package/dist/chunk-6JMORBVL.js.map +1 -0
- package/dist/{chunk-6HAIUXNZ.js → chunk-KRUCMXDD.js} +2 -2
- package/dist/chunk-KRUCMXDD.js.map +1 -0
- package/dist/components/app-dialog.d.ts +10 -1
- package/dist/components/app-dialog.js +2 -2
- package/dist/components/close-confirm-dialog.js +2 -2
- package/dist/components/confirm-by-typing-dialog.js +2 -2
- package/dist/components/confirm-dialog.js +2 -2
- package/dist/components/image-crop-panel.d.ts +0 -6
- package/dist/components/image-crop-panel.js +91 -97
- package/dist/components/image-crop-panel.js.map +1 -1
- package/dist/components/plugin-download-consent.d.ts +1 -1
- package/dist/components/plugin-download-consent.js +2 -2
- package/dist/components/plugin-download-consent.js.map +1 -1
- package/dist/hooks/use-dialog-footer-actions.d.ts +8 -0
- package/dist/hooks/use-dialog-footer-actions.js +1 -1
- package/dist/index.js +2 -2
- package/package.json +3 -3
- package/src/components/app-dialog.test.tsx +74 -0
- package/src/components/app-dialog.tsx +41 -3
- package/src/components/image-crop-panel.test.tsx +24 -6
- package/src/components/image-crop-panel.tsx +16 -29
- package/src/components/plugin-download-consent.tsx +1 -1
- package/src/hooks/use-dialog-footer-actions.ts +8 -0
- package/dist/chunk-2BVZEYNW.js.map +0 -1
- package/dist/chunk-6HAIUXNZ.js.map +0 -1
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import { useDialogFooterActions, DialogFooterActionsContext } from './chunk-
|
|
1
|
+
import { useDialogFooterActions, DialogFooterActionsContext } from './chunk-KRUCMXDD.js';
|
|
2
2
|
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription, DialogBody, DialogFooter } from './chunk-KOFWEPPB.js';
|
|
3
3
|
import { cn } from './chunk-M5OJN54O.js';
|
|
4
4
|
import { useEffect, useRef, useState, useMemo } from 'react';
|
|
5
|
-
import { jsx, jsxs } from 'react/jsx-runtime';
|
|
5
|
+
import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
|
|
6
6
|
|
|
7
7
|
var sizeClasses = {
|
|
8
8
|
sm: "sm:max-w-sm",
|
|
@@ -20,6 +20,17 @@ function DialogFooterActions({ children }) {
|
|
|
20
20
|
}, [slot, children]);
|
|
21
21
|
return null;
|
|
22
22
|
}
|
|
23
|
+
function DialogFooterLeadingActions({
|
|
24
|
+
children
|
|
25
|
+
}) {
|
|
26
|
+
const slot = useDialogFooterActions();
|
|
27
|
+
useEffect(() => {
|
|
28
|
+
if (slot === null) return;
|
|
29
|
+
slot.setLeadingActions(children);
|
|
30
|
+
return () => slot.setLeadingActions(null);
|
|
31
|
+
}, [slot, children]);
|
|
32
|
+
return null;
|
|
33
|
+
}
|
|
23
34
|
function AppDialog(props) {
|
|
24
35
|
const {
|
|
25
36
|
open,
|
|
@@ -41,9 +52,10 @@ function AppDialog(props) {
|
|
|
41
52
|
} = props;
|
|
42
53
|
const contentRef = useRef(null);
|
|
43
54
|
const [footerActions, setFooterActions] = useState(null);
|
|
55
|
+
const [leadingActions, setLeadingActions] = useState(null);
|
|
44
56
|
const actionsValue = useMemo(
|
|
45
|
-
() => ({ setFooterActions }),
|
|
46
|
-
[setFooterActions]
|
|
57
|
+
() => ({ setFooterActions, setLeadingActions }),
|
|
58
|
+
[setFooterActions, setLeadingActions]
|
|
47
59
|
);
|
|
48
60
|
return /* @__PURE__ */ jsx(Dialog, { open, onOpenChange, children: /* @__PURE__ */ jsxs(
|
|
49
61
|
DialogContent,
|
|
@@ -83,15 +95,26 @@ function AppDialog(props) {
|
|
|
83
95
|
description !== void 0 ? /* @__PURE__ */ jsx(DialogDescription, { children: description }) : null
|
|
84
96
|
] }),
|
|
85
97
|
/* @__PURE__ */ jsx(DialogBody, { className: flush ? "p-0" : void 0, children: /* @__PURE__ */ jsx(DialogFooterActionsContext.Provider, { value: actionsValue, children }) }),
|
|
86
|
-
footer !== void 0 && footer !== null || footerActions !== null
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
98
|
+
footer !== void 0 && footer !== null || footerActions !== null || leadingActions !== null ? (
|
|
99
|
+
// Footer-action placement (DESIGN.md — dialog anatomy):
|
|
100
|
+
// with a primary action the bar splits — the leading
|
|
101
|
+
// function key sits at the left edge while cancel + the
|
|
102
|
+
// primary stay right-aligned ([remove] [cancel] [save]);
|
|
103
|
+
// without a primary the bar never splits — cancel leads
|
|
104
|
+
// and the function key holds the right edge ([cancel]
|
|
105
|
+
// [remove]).
|
|
106
|
+
/* @__PURE__ */ jsxs(DialogFooter, { flush, children: [
|
|
107
|
+
leadingActions !== null && footerActions !== null ? /* @__PURE__ */ jsx("div", { className: "mr-auto", children: leadingActions }) : null,
|
|
108
|
+
footer,
|
|
109
|
+
footerActions,
|
|
110
|
+
leadingActions !== null && footerActions === null ? /* @__PURE__ */ jsx(Fragment, { children: leadingActions }) : null
|
|
111
|
+
] })
|
|
112
|
+
) : null
|
|
90
113
|
]
|
|
91
114
|
}
|
|
92
115
|
) });
|
|
93
116
|
}
|
|
94
117
|
|
|
95
|
-
export { AppDialog, DialogFooterActions };
|
|
96
|
-
//# sourceMappingURL=chunk-
|
|
97
|
-
//# sourceMappingURL=chunk-
|
|
118
|
+
export { AppDialog, DialogFooterActions, DialogFooterLeadingActions };
|
|
119
|
+
//# sourceMappingURL=chunk-6JMORBVL.js.map
|
|
120
|
+
//# sourceMappingURL=chunk-6JMORBVL.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/components/app-dialog.tsx"],"names":[],"mappings":"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","file":"chunk-6JMORBVL.js","sourcesContent":["import {\n\tDialog,\n\tDialogBody,\n\tDialogContent,\n\tDialogDescription,\n\tDialogFooter,\n\tDialogHeader,\n\tDialogTitle,\n} from \"./dialog\"\nimport {\n\tDialogFooterActionsContext,\n\tuseDialogFooterActions,\n} from \"@hoardodile/ui/hooks/use-dialog-footer-actions\"\nimport { cn } from \"@hoardodile/ui/lib/utils\"\nimport {\n\ttype ReactNode,\n\tuseEffect,\n\tuseMemo,\n\tuseRef,\n\tuseState,\n} from \"react\"\n\n/** Dialog width tiers: the 320–480 narrow slot plus\n the wide working tiers — edit hubs, pickers and selectors get\n real estate; confirmations never leave the narrow slot. */\nexport type AppDialogSize = \"sm\" | \"md\" | \"lg\" | \"xl\" | \"2xl\"\n\nconst sizeClasses: Record<AppDialogSize, string> = {\n\tsm: \"sm:max-w-sm\",\n\tmd: \"sm:max-w-md\",\n\tlg: \"sm:max-w-2xl\",\n\txl: \"sm:max-w-3xl\",\n\t\"2xl\": \"sm:max-w-4xl\",\n}\n\nexport { useDialogFooterActions }\n\n/**\n * Declarative footer action: renders nothing in the body and contributes\n * the node to the surrounding dialog's footer (appended after the\n * dialog's own `footer` prop). Panels embed their primary action this way\n * so action buttons live in the footer, never in the body. Outside a\n * dialog (sheets, pickers) it renders nothing — callers keep their inline\n * action bar as the fallback.\n */\nexport function DialogFooterActions({ children }: { children: ReactNode }) {\n\tconst slot = useDialogFooterActions()\n\tuseEffect(() => {\n\t\tif (slot === null) return\n\t\tslot.setFooterActions(children)\n\t\treturn () => slot.setFooterActions(null)\n\t}, [slot, children])\n\treturn null\n}\n\n/**\n * Declarative left-edge footer action: contributes the node to the\n * surrounding dialog's footer front, pushed to the left edge (cancel and\n * the primary action stay right-aligned — DESIGN.md three-button footers).\n * Renders nothing outside a dialog.\n */\nexport function DialogFooterLeadingActions({\n\tchildren,\n}: {\n\tchildren: ReactNode\n}) {\n\tconst slot = useDialogFooterActions()\n\tuseEffect(() => {\n\t\tif (slot === null) return\n\t\tslot.setLeadingActions(children)\n\t\treturn () => slot.setLeadingActions(null)\n\t}, [slot, children])\n\treturn null\n}\n\nexport type AppDialogProps = Readonly<{\n\topen: boolean\n\tonOpenChange: (open: boolean) => void\n\ttitle: ReactNode\n\t/** Uppercase section marker above the title (feature dialogs). */\n\teyebrow?: ReactNode\n\t/** Leading icon tile in the title row (feature dialogs). */\n\ticon?: ReactNode\n\t/** Danger register — the title ink takes the destructive hue; the\n\t danger itself stays in copy and the confirm button. */\n\tdanger?: boolean\n\t/** Width tier; defaults to `md` (the narrow form slot). */\n\tsize?: AppDialogSize\n\t/**\n\t * Drops the body padding for workspace bodies that own their layout\n\t * edge to edge (the two-pane editors); the header keeps its padding\n\t * and the footer becomes a plain bottom bar.\n\t */\n\tflush?: boolean\n\tdescription?: ReactNode\n\tchildren: ReactNode\n\t/** Leading footer actions; body panels may append theirs via\n\t {@link DialogFooterActions}. */\n\tfooter?: ReactNode\n\tcontentClassName?: string\n\t/** Passed to dialog overlay (e.g. disable backdrop blur over WebGL). */\n\toverlayClassName?: string\n\t/** Fade-only motion — lighter than default slide/zoom when over heavy surfaces. */\n\tcontentMotion?: \"default\" | \"minimal\"\n\t/** Defaults to true. Set to false to honour native auto-focus (rare). */\n\tsuppressAutoFocus?: boolean\n\tcontentTestId?: string\n}>\n\n/**\n * Standard dialog shell. Wires up `Dialog`/`DialogContent`/`DialogHeader`/\n * `DialogTitle` and an optional eyebrow, icon, description and footer so\n * callers stop re-stating the boilerplate. Use {@link ConfirmDialog} for\n * the common \"cancel + primary action\" pattern.\n */\nexport function AppDialog(props: AppDialogProps) {\n\tconst {\n\t\topen,\n\t\tonOpenChange,\n\t\ttitle,\n\t\teyebrow,\n\t\ticon,\n\t\tdanger,\n\t\tsize,\n\t\tflush,\n\t\tdescription,\n\t\tchildren,\n\t\tfooter,\n\t\tcontentClassName,\n\t\toverlayClassName,\n\t\tcontentMotion,\n\t\tsuppressAutoFocus = true,\n\t\tcontentTestId,\n\t} = props\n\t// Focusing the first focusable element on open would steal the caret\n\t// from the page underneath, jump scroll position, and on iOS\n\t// occasionally pop the soft keyboard before the user even reaches an\n\t// input. Routing focus to the dialog container itself keeps the\n\t// dialog accessible without those side effects.\n\tconst contentRef = useRef<HTMLDivElement | null>(null)\n\n\t// Footer actions contributed by body panels (DialogFooterActions):\n\t// appended after the dialog's own footer, reset when the dialog closes.\n\tconst [footerActions, setFooterActions] = useState<ReactNode>(null)\n\t// Left-edge function key (DialogFooterLeadingActions): rendered at the\n\t// footer's left while cancel + the primary action stay right-aligned\n\t// (DESIGN.md — three button footers). Reset when the dialog closes.\n\tconst [leadingActions, setLeadingActions] = useState<ReactNode>(null)\n\tconst actionsValue = useMemo(\n\t\t() => ({ setFooterActions, setLeadingActions }),\n\t\t[setFooterActions, setLeadingActions],\n\t)\n\n\treturn (\n\t\t<Dialog open={open} onOpenChange={onOpenChange}>\n\t\t\t<DialogContent\n\t\t\t\tref={contentRef}\n\t\t\t\tclassName={cn(\n\t\t\t\t\tsize !== undefined && sizeClasses[size],\n\t\t\t\t\tflush && \"p-0\",\n\t\t\t\t\tcontentClassName,\n\t\t\t\t)}\n\t\t\t\toverlayClassName={overlayClassName}\n\t\t\t\tcontentMotion={contentMotion}\n\t\t\t\tinitialFocus={suppressAutoFocus ? contentRef : undefined}\n\t\t\t\tdata-testid={contentTestId}\n\t\t\t>\n\t\t\t\t<DialogHeader className={flush ? \"gap-4\" : undefined}>\n\t\t\t\t\t{eyebrow !== undefined ? (\n\t\t\t\t\t\t<span\n\t\t\t\t\t\t\tclassName={cn(\n\t\t\t\t\t\t\t\t\"text-tiny font-semibold tracking-label uppercase\",\n\t\t\t\t\t\t\t\tdanger ? \"text-destructive\" : \"text-muted-foreground\",\n\t\t\t\t\t\t\t)}\n\t\t\t\t\t\t>\n\t\t\t\t\t\t\t{eyebrow}\n\t\t\t\t\t\t</span>\n\t\t\t\t\t) : null}\n\t\t\t\t\t<div className=\"flex min-w-0 items-center gap-3 pr-6\">\n\t\t\t\t\t\t{icon}\n\t\t\t\t\t\t<DialogTitle\n\t\t\t\t\t\t\tclassName={cn(\"truncate\", danger && \"text-destructive\")}\n\t\t\t\t\t\t>\n\t\t\t\t\t\t\t{title}\n\t\t\t\t\t\t</DialogTitle>\n\t\t\t\t\t</div>\n\t\t\t\t\t{description !== undefined ? (\n\t\t\t\t\t\t<DialogDescription>{description}</DialogDescription>\n\t\t\t\t\t) : null}\n\t\t\t\t</DialogHeader>\n\t\t\t\t{/* The header, body and footer must stay direct children of\n\t\t\t\t DialogContent — its arrangement logic recognises them by\n\t\t\t\t type, and a wrapper in between would re-wrap the body in a\n\t\t\t\t second DialogBody. The footer-actions context therefore\n\t\t\t\t wraps only the body's own content. */}\n\t\t\t\t<DialogBody className={flush ? \"p-0\" : undefined}>\n\t\t\t\t\t<DialogFooterActionsContext.Provider value={actionsValue}>\n\t\t\t\t\t\t{children}\n\t\t\t\t\t</DialogFooterActionsContext.Provider>\n\t\t\t\t</DialogBody>\n\t\t\t\t{(footer !== undefined && footer !== null) ||\n\t\t\t\tfooterActions !== null ||\n\t\t\t\tleadingActions !== null ? (\n\t\t\t\t\t// Footer-action placement (DESIGN.md — dialog anatomy):\n\t\t\t\t\t// with a primary action the bar splits — the leading\n\t\t\t\t\t// function key sits at the left edge while cancel + the\n\t\t\t\t\t// primary stay right-aligned ([remove] [cancel] [save]);\n\t\t\t\t\t// without a primary the bar never splits — cancel leads\n\t\t\t\t\t// and the function key holds the right edge ([cancel]\n\t\t\t\t\t// [remove]).\n\t\t\t\t\t<DialogFooter flush={flush}>\n\t\t\t\t\t\t{leadingActions !== null && footerActions !== null ? (\n\t\t\t\t\t\t\t<div className=\"mr-auto\">{leadingActions}</div>\n\t\t\t\t\t\t) : null}\n\t\t\t\t\t\t{footer}\n\t\t\t\t\t\t{footerActions}\n\t\t\t\t\t\t{leadingActions !== null && footerActions === null ? (\n\t\t\t\t\t\t\t<>{leadingActions}</>\n\t\t\t\t\t\t) : null}\n\t\t\t\t\t</DialogFooter>\n\t\t\t\t) : null}\n\t\t\t</DialogContent>\n\t\t</Dialog>\n\t)\n}\n"]}
|
|
@@ -7,5 +7,5 @@ function useDialogFooterActions() {
|
|
|
7
7
|
}
|
|
8
8
|
|
|
9
9
|
export { DialogFooterActionsContext, useDialogFooterActions };
|
|
10
|
-
//# sourceMappingURL=chunk-
|
|
11
|
-
//# sourceMappingURL=chunk-
|
|
10
|
+
//# sourceMappingURL=chunk-KRUCMXDD.js.map
|
|
11
|
+
//# sourceMappingURL=chunk-KRUCMXDD.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/hooks/use-dialog-footer-actions.ts"],"names":[],"mappings":";;;AAsBA,IAAM,0BAAA,GACL,cAA+C,IAAI;AAGpD,SAAS,sBAAA,GAA0D;AAClE,EAAA,OAAO,WAAW,0BAA0B,CAAA;AAC7C","file":"chunk-KRUCMXDD.js","sourcesContent":["import { createContext, type ReactNode, useContext } from \"react\"\n\n/**\n * Footer action slot plumbing for `AppDialog` — the context shared\n * between the dialog shell (`@hoardodile/ui/components/app-dialog`) and\n * body panels that contribute their primary action through\n * `DialogFooterActions`, so action buttons land in the footer instead of\n * the body.\n */\n\ntype DialogFooterActionsValue = {\n\t/** The right-aligned primary action slot (after the dialog's own footer). */\n\treadonly setFooterActions: (node: ReactNode | null) => void\n\t/**\n\t * The left-edge function key slot, rendered before the dialog's footer so\n\t * a destructive/secondary function key can sit at the left edge while\n\t * cancel and the primary action stay right-aligned (DESIGN.md — three\n\t * button footers).\n\t */\n\treadonly setLeadingActions: (node: ReactNode | null) => void\n}\n\nconst DialogFooterActionsContext =\n\tcreateContext<DialogFooterActionsValue | null>(null)\n\n/** The nearest dialog's footer action slot, or null outside a dialog. */\nfunction useDialogFooterActions(): DialogFooterActionsValue | null {\n\treturn useContext(DialogFooterActionsContext)\n}\n\nexport { DialogFooterActionsContext, useDialogFooterActions }\n"]}
|
|
@@ -18,6 +18,15 @@ type AppDialogSize = "sm" | "md" | "lg" | "xl" | "2xl";
|
|
|
18
18
|
declare function DialogFooterActions({ children }: {
|
|
19
19
|
children: ReactNode;
|
|
20
20
|
}): null;
|
|
21
|
+
/**
|
|
22
|
+
* Declarative left-edge footer action: contributes the node to the
|
|
23
|
+
* surrounding dialog's footer front, pushed to the left edge (cancel and
|
|
24
|
+
* the primary action stay right-aligned — DESIGN.md three-button footers).
|
|
25
|
+
* Renders nothing outside a dialog.
|
|
26
|
+
*/
|
|
27
|
+
declare function DialogFooterLeadingActions({ children, }: {
|
|
28
|
+
children: ReactNode;
|
|
29
|
+
}): null;
|
|
21
30
|
type AppDialogProps = Readonly<{
|
|
22
31
|
open: boolean;
|
|
23
32
|
onOpenChange: (open: boolean) => void;
|
|
@@ -59,4 +68,4 @@ type AppDialogProps = Readonly<{
|
|
|
59
68
|
*/
|
|
60
69
|
declare function AppDialog(props: AppDialogProps): React.JSX.Element;
|
|
61
70
|
|
|
62
|
-
export { AppDialog, type AppDialogProps, type AppDialogSize, DialogFooterActions };
|
|
71
|
+
export { AppDialog, type AppDialogProps, type AppDialogSize, DialogFooterActions, DialogFooterLeadingActions };
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
export { AppDialog, DialogFooterActions } from '../chunk-
|
|
2
|
-
export { useDialogFooterActions } from '../chunk-
|
|
1
|
+
export { AppDialog, DialogFooterActions, DialogFooterLeadingActions } from '../chunk-6JMORBVL.js';
|
|
2
|
+
export { useDialogFooterActions } from '../chunk-KRUCMXDD.js';
|
|
3
3
|
import '../chunk-KOFWEPPB.js';
|
|
4
4
|
import '../chunk-TDDHQZ62.js';
|
|
5
5
|
import '../chunk-QGMDWGJT.js';
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { Checkbox } from '../chunk-3ZQJWBVB.js';
|
|
2
|
-
import { AppDialog } from '../chunk-
|
|
3
|
-
import '../chunk-
|
|
2
|
+
import { AppDialog } from '../chunk-6JMORBVL.js';
|
|
3
|
+
import '../chunk-KRUCMXDD.js';
|
|
4
4
|
import '../chunk-KOFWEPPB.js';
|
|
5
5
|
import '../chunk-TDDHQZ62.js';
|
|
6
6
|
import '../chunk-QGMDWGJT.js';
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { Input } from '../chunk-UPWHCG4S.js';
|
|
2
|
-
import { AppDialog } from '../chunk-
|
|
3
|
-
import '../chunk-
|
|
2
|
+
import { AppDialog } from '../chunk-6JMORBVL.js';
|
|
3
|
+
import '../chunk-KRUCMXDD.js';
|
|
4
4
|
import '../chunk-KOFWEPPB.js';
|
|
5
5
|
import '../chunk-TDDHQZ62.js';
|
|
6
6
|
import '../chunk-QGMDWGJT.js';
|
|
@@ -17,12 +17,6 @@ type ImageCropPanelProps = {
|
|
|
17
17
|
*/
|
|
18
18
|
readonly onSave: (cropped: CroppedImage) => Promise<unknown>;
|
|
19
19
|
readonly onSaved?: () => void;
|
|
20
|
-
/**
|
|
21
|
-
* Called when the user clicks the action button but no image has been
|
|
22
|
-
* selected. The parent (e.g. {@link ImageEditPanel}) can treat this as a
|
|
23
|
-
* request to remove the existing image.
|
|
24
|
-
*/
|
|
25
|
-
readonly onClear?: () => void;
|
|
26
20
|
/**
|
|
27
21
|
* When set (e.g. canvas snapshot object URL), skips the empty picker and
|
|
28
22
|
* shows the cropper immediately.
|
|
@@ -3,15 +3,15 @@ import '../chunk-WI6O7GZK.js';
|
|
|
3
3
|
import { Label } from '../chunk-FH5RNCFR.js';
|
|
4
4
|
import { ImageCropper } from '../chunk-SKDPGSRL.js';
|
|
5
5
|
import { Switch } from '../chunk-OW5FWLPH.js';
|
|
6
|
-
import { DialogFooterActions } from '../chunk-
|
|
7
|
-
import { useDialogFooterActions } from '../chunk-
|
|
6
|
+
import { DialogFooterActions } from '../chunk-6JMORBVL.js';
|
|
7
|
+
import { useDialogFooterActions } from '../chunk-KRUCMXDD.js';
|
|
8
8
|
import '../chunk-KOFWEPPB.js';
|
|
9
9
|
import '../chunk-TDDHQZ62.js';
|
|
10
10
|
import '../chunk-QGMDWGJT.js';
|
|
11
11
|
import '../chunk-XBPQEZES.js';
|
|
12
12
|
import '../chunk-KNPRUZ4H.js';
|
|
13
13
|
import { Button } from '../chunk-PWJBHUM2.js';
|
|
14
|
-
import {
|
|
14
|
+
import { RefreshCircle, Upload, Gallery } from '../chunk-7MWWHI6T.js';
|
|
15
15
|
import '../chunk-OEVALKQ5.js';
|
|
16
16
|
import { cn } from '../chunk-M5OJN54O.js';
|
|
17
17
|
import '../chunk-2HWSCY4T.js';
|
|
@@ -29,7 +29,6 @@ function ImageCropPanel(props) {
|
|
|
29
29
|
previewHeight: previewHeightProp,
|
|
30
30
|
onSave,
|
|
31
31
|
onSaved,
|
|
32
|
-
onClear,
|
|
33
32
|
initialSrc,
|
|
34
33
|
allowChangeSource = true,
|
|
35
34
|
onSavingChange,
|
|
@@ -151,10 +150,7 @@ function ImageCropPanel(props) {
|
|
|
151
150
|
}
|
|
152
151
|
async function handleSave() {
|
|
153
152
|
const render = renderRef.current;
|
|
154
|
-
if (render === void 0)
|
|
155
|
-
onClear?.();
|
|
156
|
-
return;
|
|
157
|
-
}
|
|
153
|
+
if (render === void 0) return;
|
|
158
154
|
setSaving(true);
|
|
159
155
|
onSavingChange?.(true);
|
|
160
156
|
try {
|
|
@@ -178,9 +174,9 @@ function ImageCropPanel(props) {
|
|
|
178
174
|
{
|
|
179
175
|
type: "button",
|
|
180
176
|
onClick: handleSave,
|
|
181
|
-
disabled: saving,
|
|
177
|
+
disabled: saving || imageSrc === void 0,
|
|
182
178
|
"data-testid": "image-crop-save",
|
|
183
|
-
children: saving ? t("imageCrop.saving") :
|
|
179
|
+
children: saving ? t("imageCrop.saving") : t("imageCrop.save")
|
|
184
180
|
}
|
|
185
181
|
);
|
|
186
182
|
return /* @__PURE__ */ jsxs("div", { className: cn("flex flex-col gap-4", panelClassName), children: [
|
|
@@ -195,6 +191,20 @@ function ImageCropPanel(props) {
|
|
|
195
191
|
"data-testid": "image-crop-file-input"
|
|
196
192
|
}
|
|
197
193
|
) : null,
|
|
194
|
+
imageSrc !== void 0 && allowChangeSource ? /* @__PURE__ */ jsxs(
|
|
195
|
+
Button,
|
|
196
|
+
{
|
|
197
|
+
type: "button",
|
|
198
|
+
variant: "secondary",
|
|
199
|
+
onClick: handleReselect,
|
|
200
|
+
className: "self-center",
|
|
201
|
+
"data-testid": "image-crop-reselect",
|
|
202
|
+
children: [
|
|
203
|
+
/* @__PURE__ */ jsx(RefreshCircle, { className: "h-4 w-4" }),
|
|
204
|
+
t("imageCrop.reselect")
|
|
205
|
+
]
|
|
206
|
+
}
|
|
207
|
+
) : null,
|
|
198
208
|
/* @__PURE__ */ jsxs(
|
|
199
209
|
"div",
|
|
200
210
|
{
|
|
@@ -203,7 +213,7 @@ function ImageCropPanel(props) {
|
|
|
203
213
|
containerClassName
|
|
204
214
|
),
|
|
205
215
|
children: [
|
|
206
|
-
/* @__PURE__ */
|
|
216
|
+
/* @__PURE__ */ jsx(
|
|
207
217
|
"div",
|
|
208
218
|
{
|
|
209
219
|
className: cn(
|
|
@@ -211,92 +221,76 @@ function ImageCropPanel(props) {
|
|
|
211
221
|
stageAspectRatio !== void 0 && "w-full",
|
|
212
222
|
cropColumnClassName
|
|
213
223
|
),
|
|
214
|
-
children:
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
),
|
|
285
|
-
imageSrc !== void 0 && allowChangeSource ? /* @__PURE__ */ jsxs(
|
|
286
|
-
Button,
|
|
287
|
-
{
|
|
288
|
-
type: "button",
|
|
289
|
-
size: "sm",
|
|
290
|
-
variant: "ghost",
|
|
291
|
-
onClick: handleReselect,
|
|
292
|
-
className: "text-muted-foreground text-xs",
|
|
293
|
-
children: [
|
|
294
|
-
/* @__PURE__ */ jsx(RefreshCircle, { className: "mr-1 h-3.5 w-3.5" }),
|
|
295
|
-
t("imageCrop.reselect")
|
|
296
|
-
]
|
|
297
|
-
}
|
|
298
|
-
) : null
|
|
299
|
-
]
|
|
224
|
+
children: imageSrc === void 0 ? allowChangeSource ? /* @__PURE__ */ jsxs(
|
|
225
|
+
"button",
|
|
226
|
+
{
|
|
227
|
+
type: "button",
|
|
228
|
+
onClick: () => fileInputRef.current?.click(),
|
|
229
|
+
onDragOver: (e) => {
|
|
230
|
+
e.preventDefault();
|
|
231
|
+
setDragOver(true);
|
|
232
|
+
},
|
|
233
|
+
onDragLeave: () => setDragOver(false),
|
|
234
|
+
onDrop: handleDrop,
|
|
235
|
+
className: cn(
|
|
236
|
+
"flex select-none flex-col items-center justify-center gap-2 rounded-md border border-border bg-muted text-muted-foreground transition-colors duration-300 hover:bg-accent hover:text-accent-foreground",
|
|
237
|
+
dragOver && "bg-accent text-accent-foreground",
|
|
238
|
+
stageAspectRatio !== void 0 && "w-full"
|
|
239
|
+
),
|
|
240
|
+
"data-testid": "image-crop-frame",
|
|
241
|
+
style: stageAspectRatio !== void 0 ? { aspectRatio: stageAspectRatio } : {
|
|
242
|
+
width: cropStageWidth ?? 240,
|
|
243
|
+
height: cropStageHeight ?? 240
|
|
244
|
+
},
|
|
245
|
+
children: [
|
|
246
|
+
/* @__PURE__ */ jsx(Upload, { className: "size-5" }),
|
|
247
|
+
/* @__PURE__ */ jsx("p", { className: "text-sm", children: t("imageCrop.pickHint") })
|
|
248
|
+
]
|
|
249
|
+
}
|
|
250
|
+
) : /* @__PURE__ */ jsx(
|
|
251
|
+
"div",
|
|
252
|
+
{
|
|
253
|
+
className: cn(
|
|
254
|
+
"flex items-center justify-center rounded-md border border-dashed border-border text-sm text-muted-foreground",
|
|
255
|
+
stageAspectRatio !== void 0 && "w-full"
|
|
256
|
+
),
|
|
257
|
+
style: stageAspectRatio !== void 0 ? { aspectRatio: stageAspectRatio } : {
|
|
258
|
+
width: cropStageWidth ?? 240,
|
|
259
|
+
height: cropStageHeight ?? 240
|
|
260
|
+
},
|
|
261
|
+
children: t("imageCrop.pickHint")
|
|
262
|
+
}
|
|
263
|
+
) : /* @__PURE__ */ jsx(
|
|
264
|
+
"div",
|
|
265
|
+
{
|
|
266
|
+
className: cn(
|
|
267
|
+
"flex min-h-0 items-center justify-center overflow-hidden rounded-md border border-border bg-card",
|
|
268
|
+
stageAspectRatio === void 0 && "max-w-90",
|
|
269
|
+
cropStageClassName,
|
|
270
|
+
stageAspectRatio !== void 0 && "w-full"
|
|
271
|
+
),
|
|
272
|
+
style: stageAspectRatio !== void 0 ? { aspectRatio: stageAspectRatio } : {
|
|
273
|
+
minHeight: `${cropStageHeight}px`,
|
|
274
|
+
minWidth: `${cropStageWidth}px`,
|
|
275
|
+
width: effectiveCropWidth,
|
|
276
|
+
height: effectiveCropHeight,
|
|
277
|
+
maxWidth: effectiveCropWidth,
|
|
278
|
+
maxHeight: effectiveCropHeight
|
|
279
|
+
},
|
|
280
|
+
children: /* @__PURE__ */ jsx(
|
|
281
|
+
ImageCropper,
|
|
282
|
+
{
|
|
283
|
+
src: imageSrc,
|
|
284
|
+
aspect,
|
|
285
|
+
mimeType,
|
|
286
|
+
displayMaxWidth: stageAspectRatio !== void 0 ? "100%" : effectiveCropWidth,
|
|
287
|
+
displayMaxHeight: stageAspectRatio !== void 0 ? void 0 : effectiveCropHeight,
|
|
288
|
+
onCropReady: handleCropReady,
|
|
289
|
+
onPreviewChange: previewVisible ? handlePreviewChange : void 0
|
|
290
|
+
}
|
|
291
|
+
)
|
|
292
|
+
}
|
|
293
|
+
)
|
|
300
294
|
}
|
|
301
295
|
),
|
|
302
296
|
previewVisible ? /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/components/image-crop-panel.tsx"],"names":["onSave","onSaved","onSavingChange","t"],"mappings":"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","file":"image-crop-panel.js","sourcesContent":["import {\n\tDialogFooterActions,\n\tuseDialogFooterActions,\n} from \"@hoardodile/ui/components/app-dialog\"\nimport { Button } from \"@hoardodile/ui/components/button\"\nimport { Label } from \"@hoardodile/ui/components/label\"\nimport { Switch } from \"@hoardodile/ui/components/switch\"\nimport { toast } from \"@hoardodile/ui/components/toast\"\nimport { Gallery, RefreshCircle, Upload } from \"@hoardodile/ui/icons/registry\"\nimport { cn } from \"@hoardodile/ui/lib/utils\"\nimport { useCallback, useEffect, useId, useRef, useState } from \"react\"\nimport { useTranslation } from \"react-i18next\"\nimport type { CroppedImage } from \"./image-cropper\"\nimport { ImageCropper } from \"./image-cropper\"\n\nexport type ImageCropPanelProps = {\n\treadonly aspect?: number\n\treadonly mimeType?: \"image/png\" | \"image/jpeg\" | \"image/webp\"\n\treadonly accept?: string\n\treadonly previewShape?: \"square\" | \"circle\"\n\treadonly previewWidth?: number\n\treadonly previewHeight?: number\n\t/**\n\t * Called when the user clicks \"save\". Receives the cropped image\n\t * payload. Should return a promise that resolves once the upload is\n\t * complete; the panel then notifies via {@link onSaved}.\n\t *\n\t * @throws The implementation may throw to surface an error toast.\n\t */\n\treadonly onSave: (cropped: CroppedImage) => Promise<unknown>\n\treadonly onSaved?: () => void\n\t/**\n\t * Called when the user clicks the action button but no image has been\n\t * selected. The parent (e.g. {@link ImageEditPanel}) can treat this as a\n\t * request to remove the existing image.\n\t */\n\treadonly onClear?: () => void\n\t/**\n\t * When set (e.g. canvas snapshot object URL), skips the empty picker and\n\t * shows the cropper immediately.\n\t */\n\treadonly initialSrc?: string\n\t/**\n\t * When false, hides file pick / drag-drop and \"reselect\" (preset image only).\n\t */\n\treadonly allowChangeSource?: boolean\n\t/** Notifies parent when an async save starts / finishes (for dialog guards). */\n\treadonly onSavingChange?: (saving: boolean) => void\n\t/** Exact size (px) of the empty picker frame / crop stage. Overrides the default 240×240. */\n\treadonly cropStageWidth?: number\n\treadonly cropStageHeight?: number\n\t/**\n\t * When true, the right-hand live preview is hidden until the user turns\n\t * the switch on (saves work over large captures / WebGL snapshots).\n\t */\n\treadonly showPreviewSwitch?: boolean\n\t/**\n\t * When true, the live preview pane is never rendered — the crop stage is\n\t * the only surface. Used by creation forms whose panel column has no\n\t * room for a side-by-side preview.\n\t */\n\treadonly hidePreview?: boolean\n\t/**\n\t * When true, the crop stage fills its parent width and keeps the aspect\n\t * ratio implied by {@link cropStageWidth} / {@link cropStageHeight}\n\t * (full-bleed `aspect-square w-full` panels). Ignored when only\n\t * one stage dimension is given.\n\t */\n\treadonly fillWidth?: boolean\n\t/** Extra classes on the root panel wrapper. */\n\treadonly panelClassName?: string\n\t/** Extra classes on the row that holds the crop stage + preview. */\n\treadonly containerClassName?: string\n\t/** Extra classes on the column wrapping the crop stage (e.g. flex-1 min-w-0). */\n\treadonly cropColumnClassName?: string\n\t/** Extra classes on the bordered crop stage box (merged with defaults via tailwind-merge). */\n\treadonly cropStageClassName?: string\n\t/** When true, the bottom save/remove action button is hidden. */\n\treadonly hideActionButton?: boolean\n\t/**\n\t * When true, automatically calls {@link onSave} after the user finishes\n\t * adjusting the crop (debounced by 600 ms). Useful when the action button\n\t * is hidden and the parent only needs the final crop at form-submit time.\n\t */\n\treadonly autoSaveOnCrop?: boolean\n}\n\n/**\n * Inline image picker + cropper with live preview.\n */\nexport function ImageCropPanel(props: ImageCropPanelProps) {\n\tconst {\n\t\taspect,\n\t\tmimeType = \"image/png\",\n\t\taccept = \"image/*\",\n\t\tpreviewShape = \"square\",\n\t\tpreviewWidth: previewWidthProp,\n\t\tpreviewHeight: previewHeightProp,\n\t\tonSave,\n\t\tonSaved,\n\t\tonClear,\n\t\tinitialSrc,\n\t\tallowChangeSource = true,\n\t\tonSavingChange,\n\t\tcropStageWidth,\n\t\tcropStageHeight,\n\t\tshowPreviewSwitch = false,\n\t\thidePreview = false,\n\t\tfillWidth = false,\n\t\tpanelClassName,\n\t\tcontainerClassName,\n\t\tcropColumnClassName,\n\t\tcropStageClassName,\n\t\thideActionButton = false,\n\t\tautoSaveOnCrop = false,\n\t} = props\n\tconst { t } = useTranslation(\"ui\", { useSuspense: false })\n\tconst footerSlot = useDialogFooterActions()\n\tconst previewWidth = previewWidthProp ?? cropStageWidth ?? 200\n\tconst previewHeight = previewHeightProp ?? cropStageHeight ?? 200\n\tconst effectiveCropWidth = cropStageWidth ?? 320\n\tconst effectiveCropHeight = cropStageHeight ?? 240\n\t// Full-bleed stage: the aspect ratio of the requested stage dimensions,\n\t// stretched to whatever width the parent column gives us.\n\tconst stageAspectRatio =\n\t\tfillWidth && cropStageWidth !== undefined && cropStageHeight !== undefined\n\t\t\t? `${cropStageWidth} / ${cropStageHeight}`\n\t\t\t: undefined\n\tconst previewToggleId = useId()\n\tconst fileInputRef = useRef<HTMLInputElement | null>(null)\n\tconst [imageSrc, setImageSrc] = useState<string | undefined>(initialSrc)\n\tconst renderRef = useRef<(() => Promise<CroppedImage>) | undefined>(undefined)\n\tconst [previewDataUrl, setPreviewDataUrl] = useState<string | undefined>(\n\t\tundefined,\n\t)\n\tconst [previewPaneOn, setPreviewPaneOn] = useState(false)\n\tconst [dragOver, setDragOver] = useState(false)\n\tconst [saving, setSaving] = useState(false)\n\tconst previewVisible = !hidePreview && (!showPreviewSwitch || previewPaneOn)\n\tconst autoSaveTimerRef = useRef<ReturnType<typeof setTimeout> | undefined>(\n\t\tundefined,\n\t)\n\tconst callbacksRef = useRef({ onSave, onSaved, onSavingChange, t })\n\tcallbacksRef.current = { onSave, onSaved, onSavingChange, t }\n\n\tuseEffect(\n\t\tfunction syncInitialSrc() {\n\t\t\tif (initialSrc !== undefined) setImageSrc(initialSrc)\n\t\t},\n\t\t[initialSrc],\n\t)\n\n\tconst handleCropReady = useCallback(\n\t\t(render: () => Promise<CroppedImage>) => {\n\t\t\trenderRef.current = render\n\t\t\tif (autoSaveOnCrop) {\n\t\t\t\tif (autoSaveTimerRef.current) {\n\t\t\t\t\tclearTimeout(autoSaveTimerRef.current)\n\t\t\t\t}\n\t\t\t\tautoSaveTimerRef.current = setTimeout(() => {\n\t\t\t\t\trender()\n\t\t\t\t\t\t.then(async (cropped) => {\n\t\t\t\t\t\t\tconst { onSave, onSaved, onSavingChange, t } =\n\t\t\t\t\t\t\t\tcallbacksRef.current\n\t\t\t\t\t\t\ttry {\n\t\t\t\t\t\t\t\tsetSaving(true)\n\t\t\t\t\t\t\t\tonSavingChange?.(true)\n\t\t\t\t\t\t\t\tawait onSave(cropped)\n\t\t\t\t\t\t\t\tonSaved?.()\n\t\t\t\t\t\t\t} catch (err) {\n\t\t\t\t\t\t\t\tconst msg = err instanceof Error ? err.message : String(err)\n\t\t\t\t\t\t\t\ttoast.add({\n\t\t\t\t\t\t\t\t\ttitle: msg || t(\"imageCrop.saveFailed\"),\n\t\t\t\t\t\t\t\t\ttype: \"error\",\n\t\t\t\t\t\t\t\t})\n\t\t\t\t\t\t\t} finally {\n\t\t\t\t\t\t\t\tsetSaving(false)\n\t\t\t\t\t\t\t\tonSavingChange?.(false)\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t})\n\t\t\t\t\t\t.catch((err) => {\n\t\t\t\t\t\t\tconst { t } = callbacksRef.current\n\t\t\t\t\t\t\tconst msg = err instanceof Error ? err.message : String(err)\n\t\t\t\t\t\t\ttoast.add({\n\t\t\t\t\t\t\t\ttitle: msg || t(\"imageCrop.saveFailed\"),\n\t\t\t\t\t\t\t\ttype: \"error\",\n\t\t\t\t\t\t\t})\n\t\t\t\t\t\t})\n\t\t\t\t}, 600)\n\t\t\t}\n\t\t},\n\t\t[autoSaveOnCrop],\n\t)\n\n\tconst handlePreviewChange = useCallback((dataUrl: string) => {\n\t\tsetPreviewDataUrl(dataUrl)\n\t}, [])\n\n\tfunction handlePreviewToggle(on: boolean) {\n\t\tsetPreviewPaneOn(on)\n\t\tif (!on) setPreviewDataUrl(undefined)\n\t}\n\n\tfunction handleFileChange(e: React.ChangeEvent<HTMLInputElement>) {\n\t\tconst file = e.target.files?.[0]\n\t\tif (file === undefined) return\n\t\treadFileAsDataUrl(file)\n\t}\n\n\tfunction handleDrop(e: React.DragEvent<HTMLElement>) {\n\t\te.preventDefault()\n\t\tsetDragOver(false)\n\t\tconst file = e.dataTransfer.files?.[0]\n\t\tif (file !== undefined) readFileAsDataUrl(file)\n\t}\n\n\tfunction readFileAsDataUrl(file: File) {\n\t\tconst reader = new FileReader()\n\t\treader.addEventListener(\"load\", () => {\n\t\t\tconst result = reader.result\n\t\t\tif (typeof result === \"string\") setImageSrc(result)\n\t\t})\n\t\treader.readAsDataURL(file)\n\t}\n\n\tfunction handleReselect() {\n\t\tif (autoSaveTimerRef.current) {\n\t\t\tclearTimeout(autoSaveTimerRef.current)\n\t\t\tautoSaveTimerRef.current = undefined\n\t\t}\n\t\tsetImageSrc(undefined)\n\t\trenderRef.current = undefined\n\t\tsetPreviewDataUrl(undefined)\n\t\tsetPreviewPaneOn(false)\n\t\tfileInputRef.current?.click()\n\t}\n\n\tasync function handleSave() {\n\t\tconst render = renderRef.current\n\t\tif (render === undefined) {\n\t\t\tonClear?.()\n\t\t\treturn\n\t\t}\n\t\tsetSaving(true)\n\t\tonSavingChange?.(true)\n\t\ttry {\n\t\t\tconst cropped = await render()\n\t\t\tawait onSave(cropped)\n\t\t\tonSaved?.()\n\t\t} catch (err) {\n\t\t\tconst msg = err instanceof Error ? err.message : String(err)\n\t\t\ttoast.add({\n\t\t\t\ttitle: msg || t(\"imageCrop.saveFailed\"),\n\t\t\t\ttype: \"error\",\n\t\t\t})\n\t\t} finally {\n\t\t\tsetSaving(false)\n\t\t\tonSavingChange?.(false)\n\t\t}\n\t}\n\n\tconst previewRadiusClass =\n\t\tpreviewShape === \"circle\" ? \"rounded-full\" : \"rounded\"\n\n\tconst actionButton = (\n\t\t<Button\n\t\t\ttype=\"button\"\n\t\t\tonClick={handleSave}\n\t\t\tdisabled={saving}\n\t\t\tdata-testid=\"image-crop-save\"\n\t\t>\n\t\t\t{saving\n\t\t\t\t? t(\"imageCrop.saving\")\n\t\t\t\t: imageSrc === undefined\n\t\t\t\t\t? t(\"imageCrop.remove\")\n\t\t\t\t\t: t(\"imageCrop.save\")}\n\t\t</Button>\n\t)\n\n\treturn (\n\t\t<div className={cn(\"flex flex-col gap-4\", panelClassName)}>\n\t\t\t{allowChangeSource ? (\n\t\t\t\t<input\n\t\t\t\t\tref={fileInputRef}\n\t\t\t\t\ttype=\"file\"\n\t\t\t\t\taccept={accept}\n\t\t\t\t\tclassName=\"hidden\"\n\t\t\t\t\tonChange={handleFileChange}\n\t\t\t\t\tdata-testid=\"image-crop-file-input\"\n\t\t\t\t/>\n\t\t\t) : null}\n\n\t\t\t<div\n\t\t\t\tclassName={cn(\n\t\t\t\t\t\"flex flex-col items-center gap-6 py-2 sm:flex-row sm:items-center sm:justify-center\",\n\t\t\t\t\tcontainerClassName,\n\t\t\t\t)}\n\t\t\t>\n\t\t\t\t<div\n\t\t\t\t\tclassName={cn(\n\t\t\t\t\t\t\"flex flex-col items-center gap-2\",\n\t\t\t\t\t\tstageAspectRatio !== undefined && \"w-full\",\n\t\t\t\t\t\tcropColumnClassName,\n\t\t\t\t\t)}\n\t\t\t\t>\n\t\t\t\t\t{imageSrc === undefined ? (\n\t\t\t\t\t\tallowChangeSource ? (\n\t\t\t\t\t\t\t<button\n\t\t\t\t\t\t\t\ttype=\"button\"\n\t\t\t\t\t\t\t\tonClick={() => fileInputRef.current?.click()}\n\t\t\t\t\t\t\t\tonDragOver={(e) => {\n\t\t\t\t\t\t\t\t\te.preventDefault()\n\t\t\t\t\t\t\t\t\tsetDragOver(true)\n\t\t\t\t\t\t\t\t}}\n\t\t\t\t\t\t\t\tonDragLeave={() => setDragOver(false)}\n\t\t\t\t\t\t\t\tonDrop={handleDrop}\n\t\t\t\t\t\t\t\tclassName={cn(\n\t\t\t\t\t\t\t\t\t\"flex select-none flex-col items-center justify-center gap-2 rounded-md border border-border bg-muted text-muted-foreground transition-colors duration-300 hover:bg-accent hover:text-accent-foreground\",\n\t\t\t\t\t\t\t\t\tdragOver && \"bg-accent text-accent-foreground\",\n\t\t\t\t\t\t\t\t\tstageAspectRatio !== undefined && \"w-full\",\n\t\t\t\t\t\t\t\t)}\n\t\t\t\t\t\t\t\tdata-testid=\"image-crop-frame\"\n\t\t\t\t\t\t\t\tstyle={\n\t\t\t\t\t\t\t\t\tstageAspectRatio !== undefined\n\t\t\t\t\t\t\t\t\t\t? { aspectRatio: stageAspectRatio }\n\t\t\t\t\t\t\t\t\t\t: {\n\t\t\t\t\t\t\t\t\t\t\t\twidth: cropStageWidth ?? 240,\n\t\t\t\t\t\t\t\t\t\t\t\theight: cropStageHeight ?? 240,\n\t\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t<Upload className=\"size-5\" />\n\t\t\t\t\t\t\t\t<p className=\"text-sm\">{t(\"imageCrop.pickHint\")}</p>\n\t\t\t\t\t\t\t</button>\n\t\t\t\t\t\t) : (\n\t\t\t\t\t\t\t<div\n\t\t\t\t\t\t\t\tclassName={cn(\n\t\t\t\t\t\t\t\t\t\"flex items-center justify-center rounded-md border border-dashed border-border text-sm text-muted-foreground\",\n\t\t\t\t\t\t\t\t\tstageAspectRatio !== undefined && \"w-full\",\n\t\t\t\t\t\t\t\t)}\n\t\t\t\t\t\t\t\tstyle={\n\t\t\t\t\t\t\t\t\tstageAspectRatio !== undefined\n\t\t\t\t\t\t\t\t\t\t? { aspectRatio: stageAspectRatio }\n\t\t\t\t\t\t\t\t\t\t: {\n\t\t\t\t\t\t\t\t\t\t\t\twidth: cropStageWidth ?? 240,\n\t\t\t\t\t\t\t\t\t\t\t\theight: cropStageHeight ?? 240,\n\t\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t{t(\"imageCrop.pickHint\")}\n\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t)\n\t\t\t\t\t) : (\n\t\t\t\t\t\t<div\n\t\t\t\t\t\t\tclassName={cn(\n\t\t\t\t\t\t\t\t\"flex min-h-0 items-center justify-center overflow-hidden rounded-md border border-border bg-card\",\n\t\t\t\t\t\t\t\tstageAspectRatio === undefined && \"max-w-90\",\n\t\t\t\t\t\t\t\tcropStageClassName,\n\t\t\t\t\t\t\t\tstageAspectRatio !== undefined && \"w-full\",\n\t\t\t\t\t\t\t)}\n\t\t\t\t\t\t\tstyle={\n\t\t\t\t\t\t\t\tstageAspectRatio !== undefined\n\t\t\t\t\t\t\t\t\t? { aspectRatio: stageAspectRatio }\n\t\t\t\t\t\t\t\t\t: {\n\t\t\t\t\t\t\t\t\t\t\tminHeight: `${cropStageHeight}px`,\n\t\t\t\t\t\t\t\t\t\t\tminWidth: `${cropStageWidth}px`,\n\t\t\t\t\t\t\t\t\t\t\twidth: effectiveCropWidth,\n\t\t\t\t\t\t\t\t\t\t\theight: effectiveCropHeight,\n\t\t\t\t\t\t\t\t\t\t\tmaxWidth: effectiveCropWidth,\n\t\t\t\t\t\t\t\t\t\t\tmaxHeight: effectiveCropHeight,\n\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t>\n\t\t\t\t\t\t\t<ImageCropper\n\t\t\t\t\t\t\t\tsrc={imageSrc}\n\t\t\t\t\t\t\t\taspect={aspect}\n\t\t\t\t\t\t\t\tmimeType={mimeType}\n\t\t\t\t\t\t\t\tdisplayMaxWidth={\n\t\t\t\t\t\t\t\t\tstageAspectRatio !== undefined ? \"100%\" : effectiveCropWidth\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\tdisplayMaxHeight={\n\t\t\t\t\t\t\t\t\tstageAspectRatio !== undefined\n\t\t\t\t\t\t\t\t\t\t? undefined\n\t\t\t\t\t\t\t\t\t\t: effectiveCropHeight\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\tonCropReady={handleCropReady}\n\t\t\t\t\t\t\t\tonPreviewChange={\n\t\t\t\t\t\t\t\t\tpreviewVisible ? handlePreviewChange : undefined\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t/>\n\t\t\t\t\t</div>\n\t\t\t\t\t)}\n\t\t\t\t\t{imageSrc !== undefined && allowChangeSource ? (\n\t\t\t\t\t\t<Button\n\t\t\t\t\t\t\ttype=\"button\"\n\t\t\t\t\t\t\tsize=\"sm\"\n\t\t\t\t\t\t\tvariant=\"ghost\"\n\t\t\t\t\t\t\tonClick={handleReselect}\n\t\t\t\t\t\t\tclassName=\"text-muted-foreground text-xs\"\n\t\t\t\t\t\t>\n\t\t\t\t\t\t\t<RefreshCircle className=\"mr-1 h-3.5 w-3.5\" />\n\t\t\t\t\t\t\t{t(\"imageCrop.reselect\")}\n\t\t\t\t\t\t</Button>\n\t\t\t\t\t) : null}\n\t\t\t\t</div>\n\n\t\t\t\t{previewVisible ? (\n\t\t\t\t\t<>\n\t\t\t\t\t\t<div className={cn(\"hidden w-px bg-border sm:block h-40\")} />\n\t\t\t\t\t\t<div className={cn(\"block bg-border sm:hidden h-px w-32\")} />\n\n\t\t\t\t\t\t<div className={cn(\"flex shrink-0 flex-col items-center gap-1.5\")}>\n\t\t\t\t\t\t\t<div\n\t\t\t\t\t\t\t\tstyle={{ width: previewWidth, height: previewHeight }}\n\t\t\t\t\t\t\t\tclassName={cn(\n\t\t\t\t\t\t\t\t\t\"flex items-center justify-center overflow-hidden border border-border bg-muted text-muted-foreground\",\n\t\t\t\t\t\t\t\t\tpreviewRadiusClass,\n\t\t\t\t\t\t\t\t)}\n\t\t\t\t\t\t\t\tdata-testid=\"image-crop-preview\"\n\t\t\t\t\t\t\t\ttitle={t(\"imageCrop.previewLabel\")}\n\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t{previewDataUrl !== undefined ? (\n\t\t\t\t\t\t\t\t\t<img\n\t\t\t\t\t\t\t\t\t\tsrc={previewDataUrl}\n\t\t\t\t\t\t\t\t\t\talt={t(\"imageCrop.previewAlt\")}\n\t\t\t\t\t\t\t\t\t\tclassName=\"max-h-full max-w-full object-cover\"\n\t\t\t\t\t\t\t\t\t/>\n\t\t\t\t\t\t\t\t) : (\n\t\t\t\t\t\t\t\t\t<Gallery className=\"h-10 w-10 opacity-60\" />\n\t\t\t\t\t\t\t\t)}\n\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t</div>\n\t\t\t\t\t</>\n\t\t\t\t) : null}\n\t\t\t</div>\n\n\t\t\t{showPreviewSwitch && imageSrc !== undefined ? (\n\t\t\t\t<div className={cn(\"flex items-center justify-center gap-2\")}>\n\t\t\t\t\t<Switch\n\t\t\t\t\t\tid={previewToggleId}\n\t\t\t\t\t\tchecked={previewPaneOn}\n\t\t\t\t\t\tonCheckedChange={handlePreviewToggle}\n\t\t\t\t\t\tdata-testid=\"image-crop-preview-toggle\"\n\t\t\t\t\t/>\n\t\t\t\t\t<Label\n\t\t\t\t\t\thtmlFor={previewToggleId}\n\t\t\t\t\t\tclassName=\"text-muted-foreground cursor-pointer text-sm font-normal\"\n\t\t\t\t\t>\n\t\t\t\t\t\t{t(\"imageCrop.showPreview\")}\n\t\t\t\t\t</Label>\n\t\t\t\t</div>\n\t\t\t) : null}\n\n\t\t\t{/* The save/remove action lands on the surrounding dialog's\n\t\t\t footer row — side by side with Cancel. Outside a dialog it\n\t\t\t falls back to an inline row at the panel's bottom. */}\n\t\t\t{!hideActionButton &&\n\t\t\t\t(footerSlot === null ? (\n\t\t\t\t\t<div className={cn(\"flex justify-end pt-2\")}>{actionButton}</div>\n\t\t\t\t) : (\n\t\t\t\t\t<DialogFooterActions>{actionButton}</DialogFooterActions>\n\t\t\t\t))}\n\t\t</div>\n\t)\n}\n"]}
|
|
1
|
+
{"version":3,"sources":["../../src/components/image-crop-panel.tsx"],"names":["onSave","onSaved","onSavingChange","t"],"mappings":"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","file":"image-crop-panel.js","sourcesContent":["import {\n\tDialogFooterActions,\n\tuseDialogFooterActions,\n} from \"@hoardodile/ui/components/app-dialog\"\nimport { Button } from \"@hoardodile/ui/components/button\"\nimport { Label } from \"@hoardodile/ui/components/label\"\nimport { Switch } from \"@hoardodile/ui/components/switch\"\nimport { toast } from \"@hoardodile/ui/components/toast\"\nimport { Gallery, RefreshCircle, Upload } from \"@hoardodile/ui/icons/registry\"\nimport { cn } from \"@hoardodile/ui/lib/utils\"\nimport { useCallback, useEffect, useId, useRef, useState } from \"react\"\nimport { useTranslation } from \"react-i18next\"\nimport type { CroppedImage } from \"./image-cropper\"\nimport { ImageCropper } from \"./image-cropper\"\n\nexport type ImageCropPanelProps = {\n\treadonly aspect?: number\n\treadonly mimeType?: \"image/png\" | \"image/jpeg\" | \"image/webp\"\n\treadonly accept?: string\n\treadonly previewShape?: \"square\" | \"circle\"\n\treadonly previewWidth?: number\n\treadonly previewHeight?: number\n\t/**\n\t * Called when the user clicks \"save\". Receives the cropped image\n\t * payload. Should return a promise that resolves once the upload is\n\t * complete; the panel then notifies via {@link onSaved}.\n\t *\n\t * @throws The implementation may throw to surface an error toast.\n\t */\n\treadonly onSave: (cropped: CroppedImage) => Promise<unknown>\n\treadonly onSaved?: () => void\n\t/**\n\t * When set (e.g. canvas snapshot object URL), skips the empty picker and\n\t * shows the cropper immediately.\n\t */\n\treadonly initialSrc?: string\n\t/**\n\t * When false, hides file pick / drag-drop and \"reselect\" (preset image only).\n\t */\n\treadonly allowChangeSource?: boolean\n\t/** Notifies parent when an async save starts / finishes (for dialog guards). */\n\treadonly onSavingChange?: (saving: boolean) => void\n\t/** Exact size (px) of the empty picker frame / crop stage. Overrides the default 240×240. */\n\treadonly cropStageWidth?: number\n\treadonly cropStageHeight?: number\n\t/**\n\t * When true, the right-hand live preview is hidden until the user turns\n\t * the switch on (saves work over large captures / WebGL snapshots).\n\t */\n\treadonly showPreviewSwitch?: boolean\n\t/**\n\t * When true, the live preview pane is never rendered — the crop stage is\n\t * the only surface. Used by creation forms whose panel column has no\n\t * room for a side-by-side preview.\n\t */\n\treadonly hidePreview?: boolean\n\t/**\n\t * When true, the crop stage fills its parent width and keeps the aspect\n\t * ratio implied by {@link cropStageWidth} / {@link cropStageHeight}\n\t * (full-bleed `aspect-square w-full` panels). Ignored when only\n\t * one stage dimension is given.\n\t */\n\treadonly fillWidth?: boolean\n\t/** Extra classes on the root panel wrapper. */\n\treadonly panelClassName?: string\n\t/** Extra classes on the row that holds the crop stage + preview. */\n\treadonly containerClassName?: string\n\t/** Extra classes on the column wrapping the crop stage (e.g. flex-1 min-w-0). */\n\treadonly cropColumnClassName?: string\n\t/** Extra classes on the bordered crop stage box (merged with defaults via tailwind-merge). */\n\treadonly cropStageClassName?: string\n\t/** When true, the bottom save/remove action button is hidden. */\n\treadonly hideActionButton?: boolean\n\t/**\n\t * When true, automatically calls {@link onSave} after the user finishes\n\t * adjusting the crop (debounced by 600 ms). Useful when the action button\n\t * is hidden and the parent only needs the final crop at form-submit time.\n\t */\n\treadonly autoSaveOnCrop?: boolean\n}\n\n/**\n * Inline image picker + cropper with live preview.\n */\nexport function ImageCropPanel(props: ImageCropPanelProps) {\n\tconst {\n\t\taspect,\n\t\tmimeType = \"image/png\",\n\t\taccept = \"image/*\",\n\t\tpreviewShape = \"square\",\n\t\tpreviewWidth: previewWidthProp,\n\t\tpreviewHeight: previewHeightProp,\n\t\tonSave,\n\t\tonSaved,\n\t\tinitialSrc,\n\t\tallowChangeSource = true,\n\t\tonSavingChange,\n\t\tcropStageWidth,\n\t\tcropStageHeight,\n\t\tshowPreviewSwitch = false,\n\t\thidePreview = false,\n\t\tfillWidth = false,\n\t\tpanelClassName,\n\t\tcontainerClassName,\n\t\tcropColumnClassName,\n\t\tcropStageClassName,\n\t\thideActionButton = false,\n\t\tautoSaveOnCrop = false,\n\t} = props\n\tconst { t } = useTranslation(\"ui\", { useSuspense: false })\n\tconst footerSlot = useDialogFooterActions()\n\tconst previewWidth = previewWidthProp ?? cropStageWidth ?? 200\n\tconst previewHeight = previewHeightProp ?? cropStageHeight ?? 200\n\tconst effectiveCropWidth = cropStageWidth ?? 320\n\tconst effectiveCropHeight = cropStageHeight ?? 240\n\t// Full-bleed stage: the aspect ratio of the requested stage dimensions,\n\t// stretched to whatever width the parent column gives us.\n\tconst stageAspectRatio =\n\t\tfillWidth && cropStageWidth !== undefined && cropStageHeight !== undefined\n\t\t\t? `${cropStageWidth} / ${cropStageHeight}`\n\t\t\t: undefined\n\tconst previewToggleId = useId()\n\tconst fileInputRef = useRef<HTMLInputElement | null>(null)\n\tconst [imageSrc, setImageSrc] = useState<string | undefined>(initialSrc)\n\tconst renderRef = useRef<(() => Promise<CroppedImage>) | undefined>(undefined)\n\tconst [previewDataUrl, setPreviewDataUrl] = useState<string | undefined>(\n\t\tundefined,\n\t)\n\tconst [previewPaneOn, setPreviewPaneOn] = useState(false)\n\tconst [dragOver, setDragOver] = useState(false)\n\tconst [saving, setSaving] = useState(false)\n\tconst previewVisible = !hidePreview && (!showPreviewSwitch || previewPaneOn)\n\tconst autoSaveTimerRef = useRef<ReturnType<typeof setTimeout> | undefined>(\n\t\tundefined,\n\t)\n\tconst callbacksRef = useRef({ onSave, onSaved, onSavingChange, t })\n\tcallbacksRef.current = { onSave, onSaved, onSavingChange, t }\n\n\tuseEffect(\n\t\tfunction syncInitialSrc() {\n\t\t\tif (initialSrc !== undefined) setImageSrc(initialSrc)\n\t\t},\n\t\t[initialSrc],\n\t)\n\n\tconst handleCropReady = useCallback(\n\t\t(render: () => Promise<CroppedImage>) => {\n\t\t\trenderRef.current = render\n\t\t\tif (autoSaveOnCrop) {\n\t\t\t\tif (autoSaveTimerRef.current) {\n\t\t\t\t\tclearTimeout(autoSaveTimerRef.current)\n\t\t\t\t}\n\t\t\t\tautoSaveTimerRef.current = setTimeout(() => {\n\t\t\t\t\trender()\n\t\t\t\t\t\t.then(async (cropped) => {\n\t\t\t\t\t\t\tconst { onSave, onSaved, onSavingChange, t } =\n\t\t\t\t\t\t\t\tcallbacksRef.current\n\t\t\t\t\t\t\ttry {\n\t\t\t\t\t\t\t\tsetSaving(true)\n\t\t\t\t\t\t\t\tonSavingChange?.(true)\n\t\t\t\t\t\t\t\tawait onSave(cropped)\n\t\t\t\t\t\t\t\tonSaved?.()\n\t\t\t\t\t\t\t} catch (err) {\n\t\t\t\t\t\t\t\tconst msg = err instanceof Error ? err.message : String(err)\n\t\t\t\t\t\t\t\ttoast.add({\n\t\t\t\t\t\t\t\t\ttitle: msg || t(\"imageCrop.saveFailed\"),\n\t\t\t\t\t\t\t\t\ttype: \"error\",\n\t\t\t\t\t\t\t\t})\n\t\t\t\t\t\t\t} finally {\n\t\t\t\t\t\t\t\tsetSaving(false)\n\t\t\t\t\t\t\t\tonSavingChange?.(false)\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t})\n\t\t\t\t\t\t.catch((err) => {\n\t\t\t\t\t\t\tconst { t } = callbacksRef.current\n\t\t\t\t\t\t\tconst msg = err instanceof Error ? err.message : String(err)\n\t\t\t\t\t\t\ttoast.add({\n\t\t\t\t\t\t\t\ttitle: msg || t(\"imageCrop.saveFailed\"),\n\t\t\t\t\t\t\t\ttype: \"error\",\n\t\t\t\t\t\t\t})\n\t\t\t\t\t\t})\n\t\t\t\t}, 600)\n\t\t\t}\n\t\t},\n\t\t[autoSaveOnCrop],\n\t)\n\n\tconst handlePreviewChange = useCallback((dataUrl: string) => {\n\t\tsetPreviewDataUrl(dataUrl)\n\t}, [])\n\n\tfunction handlePreviewToggle(on: boolean) {\n\t\tsetPreviewPaneOn(on)\n\t\tif (!on) setPreviewDataUrl(undefined)\n\t}\n\n\tfunction handleFileChange(e: React.ChangeEvent<HTMLInputElement>) {\n\t\tconst file = e.target.files?.[0]\n\t\tif (file === undefined) return\n\t\treadFileAsDataUrl(file)\n\t}\n\n\tfunction handleDrop(e: React.DragEvent<HTMLElement>) {\n\t\te.preventDefault()\n\t\tsetDragOver(false)\n\t\tconst file = e.dataTransfer.files?.[0]\n\t\tif (file !== undefined) readFileAsDataUrl(file)\n\t}\n\n\tfunction readFileAsDataUrl(file: File) {\n\t\tconst reader = new FileReader()\n\t\treader.addEventListener(\"load\", () => {\n\t\t\tconst result = reader.result\n\t\t\tif (typeof result === \"string\") setImageSrc(result)\n\t\t})\n\t\treader.readAsDataURL(file)\n\t}\n\n\tfunction handleReselect() {\n\t\tif (autoSaveTimerRef.current) {\n\t\t\tclearTimeout(autoSaveTimerRef.current)\n\t\t\tautoSaveTimerRef.current = undefined\n\t\t}\n\t\tsetImageSrc(undefined)\n\t\trenderRef.current = undefined\n\t\tsetPreviewDataUrl(undefined)\n\t\tsetPreviewPaneOn(false)\n\t\tfileInputRef.current?.click()\n\t}\n\n\tasync function handleSave() {\n\t\tconst render = renderRef.current\n\t\tif (render === undefined) return\n\t\tsetSaving(true)\n\t\tonSavingChange?.(true)\n\t\ttry {\n\t\t\tconst cropped = await render()\n\t\t\tawait onSave(cropped)\n\t\t\tonSaved?.()\n\t\t} catch (err) {\n\t\t\tconst msg = err instanceof Error ? err.message : String(err)\n\t\t\ttoast.add({\n\t\t\t\ttitle: msg || t(\"imageCrop.saveFailed\"),\n\t\t\t\ttype: \"error\",\n\t\t\t})\n\t\t} finally {\n\t\t\tsetSaving(false)\n\t\t\tonSavingChange?.(false)\n\t\t}\n\t}\n\n\tconst previewRadiusClass =\n\t\tpreviewShape === \"circle\" ? \"rounded-full\" : \"rounded\"\n\n\tconst actionButton = (\n\t\t<Button\n\t\t\ttype=\"button\"\n\t\t\tonClick={handleSave}\n\t\t\tdisabled={saving || imageSrc === undefined}\n\t\t\tdata-testid=\"image-crop-save\"\n\t\t>\n\t\t\t{saving ? t(\"imageCrop.saving\") : t(\"imageCrop.save\")}\n\t\t</Button>\n\t)\n\n\treturn (\n\t\t<div className={cn(\"flex flex-col gap-4\", panelClassName)}>\n\t\t\t{allowChangeSource ? (\n\t\t\t\t<input\n\t\t\t\t\tref={fileInputRef}\n\t\t\t\t\ttype=\"file\"\n\t\t\t\t\taccept={accept}\n\t\t\t\t\tclassName=\"hidden\"\n\t\t\t\t\tonChange={handleFileChange}\n\t\t\t\t\tdata-testid=\"image-crop-file-input\"\n\t\t\t\t/>\n\t\t\t) : null}\n\n\t\t\t{imageSrc !== undefined && allowChangeSource ? (\n\t\t\t\t<Button\n\t\t\t\t\ttype=\"button\"\n\t\t\t\t\tvariant=\"secondary\"\n\t\t\t\t\tonClick={handleReselect}\n\t\t\t\t\tclassName=\"self-center\"\n\t\t\t\t\tdata-testid=\"image-crop-reselect\"\n\t\t\t\t>\n\t\t\t\t\t<RefreshCircle className=\"h-4 w-4\" />\n\t\t\t\t\t{t(\"imageCrop.reselect\")}\n\t\t\t\t</Button>\n\t\t\t) : null}\n\n\t\t\t<div\n\t\t\t\tclassName={cn(\n\t\t\t\t\t\"flex flex-col items-center gap-6 py-2 sm:flex-row sm:items-center sm:justify-center\",\n\t\t\t\t\tcontainerClassName,\n\t\t\t\t)}\n\t\t\t>\n\t\t\t\t<div\n\t\t\t\t\tclassName={cn(\n\t\t\t\t\t\t\"flex flex-col items-center gap-2\",\n\t\t\t\t\t\tstageAspectRatio !== undefined && \"w-full\",\n\t\t\t\t\t\tcropColumnClassName,\n\t\t\t\t\t)}\n\t\t\t\t>\n\t\t\t\t\t{imageSrc === undefined ? (\n\t\t\t\t\t\tallowChangeSource ? (\n\t\t\t\t\t\t\t<button\n\t\t\t\t\t\t\t\ttype=\"button\"\n\t\t\t\t\t\t\t\tonClick={() => fileInputRef.current?.click()}\n\t\t\t\t\t\t\t\tonDragOver={(e) => {\n\t\t\t\t\t\t\t\t\te.preventDefault()\n\t\t\t\t\t\t\t\t\tsetDragOver(true)\n\t\t\t\t\t\t\t\t}}\n\t\t\t\t\t\t\t\tonDragLeave={() => setDragOver(false)}\n\t\t\t\t\t\t\t\tonDrop={handleDrop}\n\t\t\t\t\t\t\t\tclassName={cn(\n\t\t\t\t\t\t\t\t\t\"flex select-none flex-col items-center justify-center gap-2 rounded-md border border-border bg-muted text-muted-foreground transition-colors duration-300 hover:bg-accent hover:text-accent-foreground\",\n\t\t\t\t\t\t\t\t\tdragOver && \"bg-accent text-accent-foreground\",\n\t\t\t\t\t\t\t\t\tstageAspectRatio !== undefined && \"w-full\",\n\t\t\t\t\t\t\t\t)}\n\t\t\t\t\t\t\t\tdata-testid=\"image-crop-frame\"\n\t\t\t\t\t\t\t\tstyle={\n\t\t\t\t\t\t\t\t\tstageAspectRatio !== undefined\n\t\t\t\t\t\t\t\t\t\t? { aspectRatio: stageAspectRatio }\n\t\t\t\t\t\t\t\t\t\t: {\n\t\t\t\t\t\t\t\t\t\t\t\twidth: cropStageWidth ?? 240,\n\t\t\t\t\t\t\t\t\t\t\t\theight: cropStageHeight ?? 240,\n\t\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t<Upload className=\"size-5\" />\n\t\t\t\t\t\t\t\t<p className=\"text-sm\">{t(\"imageCrop.pickHint\")}</p>\n\t\t\t\t\t\t\t</button>\n\t\t\t\t\t\t) : (\n\t\t\t\t\t\t\t<div\n\t\t\t\t\t\t\t\tclassName={cn(\n\t\t\t\t\t\t\t\t\t\"flex items-center justify-center rounded-md border border-dashed border-border text-sm text-muted-foreground\",\n\t\t\t\t\t\t\t\t\tstageAspectRatio !== undefined && \"w-full\",\n\t\t\t\t\t\t\t\t)}\n\t\t\t\t\t\t\t\tstyle={\n\t\t\t\t\t\t\t\t\tstageAspectRatio !== undefined\n\t\t\t\t\t\t\t\t\t\t? { aspectRatio: stageAspectRatio }\n\t\t\t\t\t\t\t\t\t\t: {\n\t\t\t\t\t\t\t\t\t\t\t\twidth: cropStageWidth ?? 240,\n\t\t\t\t\t\t\t\t\t\t\t\theight: cropStageHeight ?? 240,\n\t\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t{t(\"imageCrop.pickHint\")}\n\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t)\n\t\t\t\t\t) : (\n\t\t\t\t\t\t<div\n\t\t\t\t\t\t\tclassName={cn(\n\t\t\t\t\t\t\t\t\"flex min-h-0 items-center justify-center overflow-hidden rounded-md border border-border bg-card\",\n\t\t\t\t\t\t\t\tstageAspectRatio === undefined && \"max-w-90\",\n\t\t\t\t\t\t\t\tcropStageClassName,\n\t\t\t\t\t\t\t\tstageAspectRatio !== undefined && \"w-full\",\n\t\t\t\t\t\t\t)}\n\t\t\t\t\t\t\tstyle={\n\t\t\t\t\t\t\t\tstageAspectRatio !== undefined\n\t\t\t\t\t\t\t\t\t? { aspectRatio: stageAspectRatio }\n\t\t\t\t\t\t\t\t\t: {\n\t\t\t\t\t\t\t\t\t\t\tminHeight: `${cropStageHeight}px`,\n\t\t\t\t\t\t\t\t\t\t\tminWidth: `${cropStageWidth}px`,\n\t\t\t\t\t\t\t\t\t\t\twidth: effectiveCropWidth,\n\t\t\t\t\t\t\t\t\t\t\theight: effectiveCropHeight,\n\t\t\t\t\t\t\t\t\t\t\tmaxWidth: effectiveCropWidth,\n\t\t\t\t\t\t\t\t\t\t\tmaxHeight: effectiveCropHeight,\n\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t>\n\t\t\t\t\t\t\t<ImageCropper\n\t\t\t\t\t\t\t\tsrc={imageSrc}\n\t\t\t\t\t\t\t\taspect={aspect}\n\t\t\t\t\t\t\t\tmimeType={mimeType}\n\t\t\t\t\t\t\t\tdisplayMaxWidth={\n\t\t\t\t\t\t\t\t\tstageAspectRatio !== undefined ? \"100%\" : effectiveCropWidth\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\tdisplayMaxHeight={\n\t\t\t\t\t\t\t\t\tstageAspectRatio !== undefined\n\t\t\t\t\t\t\t\t\t\t? undefined\n\t\t\t\t\t\t\t\t\t\t: effectiveCropHeight\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\tonCropReady={handleCropReady}\n\t\t\t\t\t\t\t\tonPreviewChange={\n\t\t\t\t\t\t\t\t\tpreviewVisible ? handlePreviewChange : undefined\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t/>\n\t\t\t\t\t</div>\n\t\t\t\t\t)}\n\t\t\t\t</div>\n\n\t\t\t\t{previewVisible ? (\n\t\t\t\t\t<>\n\t\t\t\t\t\t<div className={cn(\"hidden w-px bg-border sm:block h-40\")} />\n\t\t\t\t\t\t<div className={cn(\"block bg-border sm:hidden h-px w-32\")} />\n\n\t\t\t\t\t\t<div className={cn(\"flex shrink-0 flex-col items-center gap-1.5\")}>\n\t\t\t\t\t\t\t<div\n\t\t\t\t\t\t\t\tstyle={{ width: previewWidth, height: previewHeight }}\n\t\t\t\t\t\t\t\tclassName={cn(\n\t\t\t\t\t\t\t\t\t\"flex items-center justify-center overflow-hidden border border-border bg-muted text-muted-foreground\",\n\t\t\t\t\t\t\t\t\tpreviewRadiusClass,\n\t\t\t\t\t\t\t\t)}\n\t\t\t\t\t\t\t\tdata-testid=\"image-crop-preview\"\n\t\t\t\t\t\t\t\ttitle={t(\"imageCrop.previewLabel\")}\n\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t{previewDataUrl !== undefined ? (\n\t\t\t\t\t\t\t\t\t<img\n\t\t\t\t\t\t\t\t\t\tsrc={previewDataUrl}\n\t\t\t\t\t\t\t\t\t\talt={t(\"imageCrop.previewAlt\")}\n\t\t\t\t\t\t\t\t\t\tclassName=\"max-h-full max-w-full object-cover\"\n\t\t\t\t\t\t\t\t\t/>\n\t\t\t\t\t\t\t\t) : (\n\t\t\t\t\t\t\t\t\t<Gallery className=\"h-10 w-10 opacity-60\" />\n\t\t\t\t\t\t\t\t)}\n\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t</div>\n\t\t\t\t\t</>\n\t\t\t\t) : null}\n\t\t\t</div>\n\n\t\t\t{showPreviewSwitch && imageSrc !== undefined ? (\n\t\t\t\t<div className={cn(\"flex items-center justify-center gap-2\")}>\n\t\t\t\t\t<Switch\n\t\t\t\t\t\tid={previewToggleId}\n\t\t\t\t\t\tchecked={previewPaneOn}\n\t\t\t\t\t\tonCheckedChange={handlePreviewToggle}\n\t\t\t\t\t\tdata-testid=\"image-crop-preview-toggle\"\n\t\t\t\t\t/>\n\t\t\t\t\t<Label\n\t\t\t\t\t\thtmlFor={previewToggleId}\n\t\t\t\t\t\tclassName=\"text-muted-foreground cursor-pointer text-sm font-normal\"\n\t\t\t\t\t>\n\t\t\t\t\t\t{t(\"imageCrop.showPreview\")}\n\t\t\t\t\t</Label>\n\t\t\t\t</div>\n\t\t\t) : null}\n\n\t\t\t{/* The save/remove action lands on the surrounding dialog's\n\t\t\t footer row — side by side with Cancel. Outside a dialog it\n\t\t\t falls back to an inline row at the panel's bottom. */}\n\t\t\t{!hideActionButton &&\n\t\t\t\t(footerSlot === null ? (\n\t\t\t\t\t<div className={cn(\"flex justify-end pt-2\")}>{actionButton}</div>\n\t\t\t\t) : (\n\t\t\t\t\t<DialogFooterActions>{actionButton}</DialogFooterActions>\n\t\t\t\t))}\n\t\t</div>\n\t)\n}\n"]}
|
|
@@ -58,7 +58,7 @@ type PluginDownloadConsentDialogProps = {
|
|
|
58
58
|
* and the plugin's stated reason, with Allow / Deny and a session-remember
|
|
59
59
|
* checkbox. A batch is one dialog listing all items.
|
|
60
60
|
*
|
|
61
|
-
* Design contract (hd-
|
|
61
|
+
* Design contract (hd-design): card + hairline + `--radius-2xl`
|
|
62
62
|
* + `--shadow-dialog`, footer parted by an inset hairline, focus on the
|
|
63
63
|
* dialog container; the danger stays in copy and actions, never in color.
|
|
64
64
|
*/
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { Checkbox } from '../chunk-3ZQJWBVB.js';
|
|
2
|
-
import { AppDialog } from '../chunk-
|
|
3
|
-
import '../chunk-
|
|
2
|
+
import { AppDialog } from '../chunk-6JMORBVL.js';
|
|
3
|
+
import '../chunk-KRUCMXDD.js';
|
|
4
4
|
import '../chunk-KOFWEPPB.js';
|
|
5
5
|
import '../chunk-TDDHQZ62.js';
|
|
6
6
|
import '../chunk-QGMDWGJT.js';
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/components/plugin-download-consent.tsx"],"names":[],"mappings":"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","file":"plugin-download-consent.js","sourcesContent":["import { Button } from \"./button.tsx\"\nimport { Checkbox } from \"./checkbox.tsx\"\nimport { AppDialog } from \"./app-dialog.tsx\"\nimport { useEffect, useState } from \"react\"\nimport { useTranslation } from \"react-i18next\"\n\n/**\n * The shared plugin asset-download consent dialog — host chrome used by\n * both host surfaces (the app and the workbench) so the user sees one\n * dialog with one contract wherever a plugin asks to download.\n *\n * One entry = one batch question: a single download is an items array of\n * one (rendered with the single-file layout); a batched plugin call is\n * one dialog listing every item, answered all-or-nothing.\n *\n * Presentational by design: the queue lives in the host's consent store\n * (`@hoardodile/host-web`), this component renders exactly one ticket\n * and reports the decision through callbacks. Copy comes from the shared\n * `ui` catalog namespace (`pluginDownload.*`), so every React surface\n * renders the same localized strings; nothing here imports anything\n * outside `@hoardodile/ui` + `@hoardodile/i18n`, keeping the SDK closure\n * intact.\n */\n\n/**\n * A queued consent ticket, structurally identical to the server's\n * `pluginDownloadRequested` SSE event minus its `type` discriminator\n * (and to `@hoardodile/host-web`'s consent-store entry). Declared here\n * instead of imported so this package stays closure-clean.\n */\nexport type PluginConsentItem = {\n\treadonly url: string\n\treadonly dest: string\n\treadonly sizeBytes?: number\n\treadonly reason?: string\n}\n\nexport type PluginConsentTicket = {\n\treadonly ticketId: string\n\treadonly pluginId: string\n\treadonly pluginName: string\n\treadonly items: readonly PluginConsentItem[]\n}\n\nexport type PluginDownloadConsentDialogProps = {\n\t/** Ticket to show; `null` hides the dialog (one at a time). */\n\treadonly entry: PluginConsentTicket | null\n\t/** Decision callbacks — the host wires them to its own decide path. */\n\treadonly onDeny: (ticketId: string) => void\n\treadonly onAllow: (ticketId: string, remember: boolean) => void\n\t/** Byte formatter; defaults to `\"<n> B\"`. */\n\treadonly formatBytes?: (bytes: number) => string\n\t/**\n\t * Overlay stacking override. Hosts with a layer above the standard\n\t * dialog stack (e.g. the app's plugin preview window at z-60) pass a\n\t * higher value so the consent question stays visible and clickable.\n\t */\n\treadonly overlayClassName?: string\n\t/** Content stacking override; see {@link overlayClassName}. */\n\treadonly contentClassName?: string\n}\n\n/**\n * Renders the shared consent question: every download URL verbatim\n * (selectable monospace, never a link), its vault-relative destination\n * and the plugin's stated reason, with Allow / Deny and a session-remember\n * checkbox. A batch is one dialog listing all items.\n *\n * Design contract (hd-plugin-design): card + hairline + `--radius-2xl`\n * + `--shadow-dialog`, footer parted by an inset hairline, focus on the\n * dialog container; the danger stays in copy and actions, never in color.\n */\nexport function PluginDownloadConsentDialog(\n\tprops: PluginDownloadConsentDialogProps,\n) {\n\tconst { entry, onDeny, onAllow } = props\n\tconst { t } = useTranslation(\"ui\", { useSuspense: false })\n\tconst formatBytes = props.formatBytes ?? ((bytes: number) => `${bytes} B`)\n\tconst activeId = entry === null ? null : entry.ticketId\n\tconst itemCount = entry === null ? 0 : entry.items.length\n\tconst [remember, setRemember] = useState(false)\n\n\t// A fresh ticket starts with \"remember\" unchecked — the choice must\n\t// never leak from one download to the next.\n\tuseEffect(() => {\n\t\tsetRemember(false)\n\t}, [activeId])\n\n\treturn (\n\t\t<AppDialog\n\t\t\topen={entry !== null}\n\t\t\tonOpenChange={(open) => {\n\t\t\t\tif (!open && activeId !== null) onDeny(activeId)\n\t\t\t}}\n\t\t\toverlayClassName={props.overlayClassName}\n\t\t\tcontentClassName={props.contentClassName}\n\t\t\ttitle={\n\t\t\t\tentry === null || itemCount <= 1\n\t\t\t\t\t? t(\"pluginDownload.title\", undefined)\n\t\t\t\t\t: t(\"pluginDownload.titleMany\", { count: itemCount })\n\t\t\t}\n\t\t\teyebrow={t(\"pluginDownload.eyebrow\", undefined)}\n\t\t\tdescription={\n\t\t\t\tentry === null\n\t\t\t\t\t? undefined\n\t\t\t\t\t: itemCount <= 1\n\t\t\t\t\t\t? t(\"pluginDownload.description\", {\n\t\t\t\t\t\t\t\tpluginName: entry.pluginName,\n\t\t\t\t\t\t\t})\n\t\t\t\t\t\t: t(\"pluginDownload.descriptionMany\", {\n\t\t\t\t\t\t\t\tpluginName: entry.pluginName,\n\t\t\t\t\t\t\t\tcount: itemCount,\n\t\t\t\t\t\t\t})\n\t\t\t}\n\t\t\tsize=\"md\"\n\t\t\tcontentTestId=\"plugin-download-consent\"\n\t\t\tfooter={\n\t\t\t\t<div className=\"flex w-full items-center justify-between\">\n\t\t\t\t\t<Button\n\t\t\t\t\t\tvariant=\"outline\"\n\t\t\t\t\t\tsize=\"sm\"\n\t\t\t\t\t\tdisabled={activeId === null}\n\t\t\t\t\t\tonClick={() => activeId !== null && onDeny(activeId)}\n\t\t\t\t\t\tdata-testid=\"plugin-download-deny\"\n\t\t\t\t\t>\n\t\t\t\t\t\t{t(\"pluginDownload.deny\", undefined)}\n\t\t\t\t\t</Button>\n\t\t\t\t\t<Button\n\t\t\t\t\t\tsize=\"sm\"\n\t\t\t\t\t\tdisabled={activeId === null}\n\t\t\t\t\t\tonClick={() =>\n\t\t\t\t\t\t\tactiveId !== null && onAllow(activeId, remember)\n\t\t\t\t\t\t}\n\t\t\t\t\t\tdata-testid=\"plugin-download-allow\"\n\t\t\t\t\t>\n\t\t\t\t\t\t{t(\"pluginDownload.allow\", undefined)}\n\t\t\t\t\t</Button>\n\t\t\t\t</div>\n\t\t\t}\n\t\t>\n\t\t\t{entry === null ? null : itemCount <= 1 ? (\n\t\t\t\t<ConsentBody\n\t\t\t\t\tentry={entry.items[0]!}\n\t\t\t\t\tremember={remember}\n\t\t\t\t\tonRememberChange={setRemember}\n\t\t\t\t\tformatBytes={formatBytes}\n\t\t\t\t/>\n\t\t\t) : (\n\t\t\t\t<ConsentBatch\n\t\t\t\t\titems={entry.items}\n\t\t\t\t\tremember={remember}\n\t\t\t\t\tonRememberChange={setRemember}\n\t\t\t\t/>\n\t\t\t)}\n\t\t</AppDialog>\n\t)\n}\n\nfunction ConsentBody(props: {\n\treadonly entry: PluginConsentItem\n\treadonly remember: boolean\n\treadonly onRememberChange: (value: boolean) => void\n\treadonly formatBytes: (bytes: number) => string\n}) {\n\tconst { entry, remember, onRememberChange, formatBytes } = props\n\tconst { t } = useTranslation(\"ui\", { useSuspense: false })\n\treturn (\n\t\t<div className=\"flex flex-col gap-4\">\n\t\t\t<ConsentUrlField label={t(\"pluginDownload.urlLabel\", undefined)} url={entry.url} />\n\t\t\t<div className=\"flex flex-col gap-1.5\">\n\t\t\t\t<span className=\"text-xs tracking-label text-muted-foreground uppercase\">\n\t\t\t\t\t{t(\"pluginDownload.destLabel\", undefined)}\n\t\t\t\t</span>\n\t\t\t\t<code\n\t\t\t\t\tclassName=\"text-xs text-secondary-foreground break-all select-all\"\n\t\t\t\t\tdata-testid=\"plugin-download-dest\"\n\t\t\t\t>\n\t\t\t\t\t{entry.dest}\n\t\t\t\t</code>\n\t\t\t</div>\n\t\t\t<div className=\"text-xs text-muted-foreground\">\n\t\t\t\t{t(\"pluginDownload.sizeLabel\", undefined)}:{\" \"}\n\t\t\t\t{entry.sizeBytes === undefined\n\t\t\t\t\t? t(\"pluginDownload.unknownSize\", undefined)\n\t\t\t\t\t: formatBytes(entry.sizeBytes)}\n\t\t\t</div>\n\t\t\t{entry.reason !== undefined && entry.reason.length > 0 ? (\n\t\t\t\t<p className=\"text-xs text-secondary-foreground italic\">\n\t\t\t\t\t{entry.reason}\n\t\t\t\t</p>\n\t\t\t) : null}\n\t\t\t<RememberRow remember={remember} onRememberChange={onRememberChange} />\n\t\t</div>\n\t)\n}\n\n/** Batch layout: one row per download, URL verbatim + dest + reason. */\nfunction ConsentBatch(props: {\n\treadonly items: readonly PluginConsentItem[]\n\treadonly remember: boolean\n\treadonly onRememberChange: (value: boolean) => void\n}) {\n\tconst { items, remember, onRememberChange } = props\n\tconst { t } = useTranslation(\"ui\", { useSuspense: false })\n\treturn (\n\t\t<div className=\"flex flex-col gap-4\">\n\t\t\t<ul className=\"flex max-h-52 flex-col gap-3 overflow-y-auto pr-1\">\n\t\t\t\t{items.map((item, index) => (\n\t\t\t\t\t<li\n\t\t\t\t\t\tkey={`${item.url}#${item.dest}`}\n\t\t\t\t\t\tclassName=\"flex flex-col gap-1 rounded-md border border-border px-2.5 py-2\"\n\t\t\t\t\t\tdata-testid={`plugin-download-item-${index}`}\n\t\t\t\t\t>\n\t\t\t\t\t\t<ConsentUrlField\n\t\t\t\t\t\t\tlabel={t(\"pluginDownload.itemLabel\", {\n\t\t\t\t\t\t\t\tindex: index + 1,\n\t\t\t\t\t\t\t})}\n\t\t\t\t\t\t\turl={item.url}\n\t\t\t\t\t\t\ttestId={`plugin-download-url-${index}`}\n\t\t\t\t\t\t/>\n\t\t\t\t\t\t<code\n\t\t\t\t\t\t\tclassName=\"text-xs text-secondary-foreground break-all select-all\"\n\t\t\t\t\t\t\tdata-testid={`plugin-download-dest-${index}`}\n\t\t\t\t\t\t>\n\t\t\t\t\t\t\t{item.dest}\n\t\t\t\t\t\t</code>\n\t\t\t\t\t\t{item.reason !== undefined && item.reason.length > 0 ? (\n\t\t\t\t\t\t\t<p className=\"text-xs text-secondary-foreground italic\">\n\t\t\t\t\t\t\t\t{item.reason}\n\t\t\t\t\t\t\t</p>\n\t\t\t\t\t\t) : null}\n\t\t\t\t\t</li>\n\t\t\t\t))}\n\t\t\t</ul>\n\t\t\t<p className=\"text-xs text-muted-foreground\">\n\t\t\t\t{t(\"pluginDownload.batchNote\", { count: items.length })}\n\t\t\t</p>\n\t\t\t<RememberRow remember={remember} onRememberChange={onRememberChange} />\n\t\t</div>\n\t)\n}\n\nfunction ConsentUrlField(props: {\n\treadonly label: string\n\treadonly url: string\n\treadonly testId?: string\n}) {\n\treturn (\n\t\t<div className=\"flex min-w-0 flex-col gap-1\">\n\t\t\t<span className=\"text-xs tracking-label text-muted-foreground uppercase\">\n\t\t\t\t{props.label}\n\t\t\t</span>\n\t\t\t<code\n\t\t\t\tclassName=\"rounded-sm bg-muted px-2 py-1.5 text-xs text-secondary-foreground break-all select-all\"\n\t\t\t\tdata-testid={props.testId ?? \"plugin-download-url\"}\n\t\t\t>\n\t\t\t\t{props.url}\n\t\t\t</code>\n\t\t</div>\n\t)\n}\n\nfunction RememberRow(props: {\n\treadonly remember: boolean\n\treadonly onRememberChange: (value: boolean) => void\n}) {\n\tconst { remember, onRememberChange } = props\n\tconst { t } = useTranslation(\"ui\", { useSuspense: false })\n\treturn (\n\t\t<div className=\"flex items-center gap-2 border-t pt-3\">\n\t\t\t<Checkbox\n\t\t\t\tchecked={remember}\n\t\t\t\tonCheckedChange={(checked) => onRememberChange(checked === true)}\n\t\t\t\tid=\"plugin-download-remember\"\n\t\t\t/>\n\t\t\t<label\n\t\t\t\thtmlFor=\"plugin-download-remember\"\n\t\t\t\tclassName=\"text-xs text-secondary-foreground select-none\"\n\t\t\t>\n\t\t\t\t{t(\"pluginDownload.remember\", undefined)}\n\t\t\t</label>\n\t\t</div>\n\t)\n}\n"]}
|
|
1
|
+
{"version":3,"sources":["../../src/components/plugin-download-consent.tsx"],"names":[],"mappings":"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","file":"plugin-download-consent.js","sourcesContent":["import { Button } from \"./button.tsx\"\nimport { Checkbox } from \"./checkbox.tsx\"\nimport { AppDialog } from \"./app-dialog.tsx\"\nimport { useEffect, useState } from \"react\"\nimport { useTranslation } from \"react-i18next\"\n\n/**\n * The shared plugin asset-download consent dialog — host chrome used by\n * both host surfaces (the app and the workbench) so the user sees one\n * dialog with one contract wherever a plugin asks to download.\n *\n * One entry = one batch question: a single download is an items array of\n * one (rendered with the single-file layout); a batched plugin call is\n * one dialog listing every item, answered all-or-nothing.\n *\n * Presentational by design: the queue lives in the host's consent store\n * (`@hoardodile/host-web`), this component renders exactly one ticket\n * and reports the decision through callbacks. Copy comes from the shared\n * `ui` catalog namespace (`pluginDownload.*`), so every React surface\n * renders the same localized strings; nothing here imports anything\n * outside `@hoardodile/ui` + `@hoardodile/i18n`, keeping the SDK closure\n * intact.\n */\n\n/**\n * A queued consent ticket, structurally identical to the server's\n * `pluginDownloadRequested` SSE event minus its `type` discriminator\n * (and to `@hoardodile/host-web`'s consent-store entry). Declared here\n * instead of imported so this package stays closure-clean.\n */\nexport type PluginConsentItem = {\n\treadonly url: string\n\treadonly dest: string\n\treadonly sizeBytes?: number\n\treadonly reason?: string\n}\n\nexport type PluginConsentTicket = {\n\treadonly ticketId: string\n\treadonly pluginId: string\n\treadonly pluginName: string\n\treadonly items: readonly PluginConsentItem[]\n}\n\nexport type PluginDownloadConsentDialogProps = {\n\t/** Ticket to show; `null` hides the dialog (one at a time). */\n\treadonly entry: PluginConsentTicket | null\n\t/** Decision callbacks — the host wires them to its own decide path. */\n\treadonly onDeny: (ticketId: string) => void\n\treadonly onAllow: (ticketId: string, remember: boolean) => void\n\t/** Byte formatter; defaults to `\"<n> B\"`. */\n\treadonly formatBytes?: (bytes: number) => string\n\t/**\n\t * Overlay stacking override. Hosts with a layer above the standard\n\t * dialog stack (e.g. the app's plugin preview window at z-60) pass a\n\t * higher value so the consent question stays visible and clickable.\n\t */\n\treadonly overlayClassName?: string\n\t/** Content stacking override; see {@link overlayClassName}. */\n\treadonly contentClassName?: string\n}\n\n/**\n * Renders the shared consent question: every download URL verbatim\n * (selectable monospace, never a link), its vault-relative destination\n * and the plugin's stated reason, with Allow / Deny and a session-remember\n * checkbox. A batch is one dialog listing all items.\n *\n * Design contract (hd-design): card + hairline + `--radius-2xl`\n * + `--shadow-dialog`, footer parted by an inset hairline, focus on the\n * dialog container; the danger stays in copy and actions, never in color.\n */\nexport function PluginDownloadConsentDialog(\n\tprops: PluginDownloadConsentDialogProps,\n) {\n\tconst { entry, onDeny, onAllow } = props\n\tconst { t } = useTranslation(\"ui\", { useSuspense: false })\n\tconst formatBytes = props.formatBytes ?? ((bytes: number) => `${bytes} B`)\n\tconst activeId = entry === null ? null : entry.ticketId\n\tconst itemCount = entry === null ? 0 : entry.items.length\n\tconst [remember, setRemember] = useState(false)\n\n\t// A fresh ticket starts with \"remember\" unchecked — the choice must\n\t// never leak from one download to the next.\n\tuseEffect(() => {\n\t\tsetRemember(false)\n\t}, [activeId])\n\n\treturn (\n\t\t<AppDialog\n\t\t\topen={entry !== null}\n\t\t\tonOpenChange={(open) => {\n\t\t\t\tif (!open && activeId !== null) onDeny(activeId)\n\t\t\t}}\n\t\t\toverlayClassName={props.overlayClassName}\n\t\t\tcontentClassName={props.contentClassName}\n\t\t\ttitle={\n\t\t\t\tentry === null || itemCount <= 1\n\t\t\t\t\t? t(\"pluginDownload.title\", undefined)\n\t\t\t\t\t: t(\"pluginDownload.titleMany\", { count: itemCount })\n\t\t\t}\n\t\t\teyebrow={t(\"pluginDownload.eyebrow\", undefined)}\n\t\t\tdescription={\n\t\t\t\tentry === null\n\t\t\t\t\t? undefined\n\t\t\t\t\t: itemCount <= 1\n\t\t\t\t\t\t? t(\"pluginDownload.description\", {\n\t\t\t\t\t\t\t\tpluginName: entry.pluginName,\n\t\t\t\t\t\t\t})\n\t\t\t\t\t\t: t(\"pluginDownload.descriptionMany\", {\n\t\t\t\t\t\t\t\tpluginName: entry.pluginName,\n\t\t\t\t\t\t\t\tcount: itemCount,\n\t\t\t\t\t\t\t})\n\t\t\t}\n\t\t\tsize=\"md\"\n\t\t\tcontentTestId=\"plugin-download-consent\"\n\t\t\tfooter={\n\t\t\t\t<div className=\"flex w-full items-center justify-between\">\n\t\t\t\t\t<Button\n\t\t\t\t\t\tvariant=\"outline\"\n\t\t\t\t\t\tsize=\"sm\"\n\t\t\t\t\t\tdisabled={activeId === null}\n\t\t\t\t\t\tonClick={() => activeId !== null && onDeny(activeId)}\n\t\t\t\t\t\tdata-testid=\"plugin-download-deny\"\n\t\t\t\t\t>\n\t\t\t\t\t\t{t(\"pluginDownload.deny\", undefined)}\n\t\t\t\t\t</Button>\n\t\t\t\t\t<Button\n\t\t\t\t\t\tsize=\"sm\"\n\t\t\t\t\t\tdisabled={activeId === null}\n\t\t\t\t\t\tonClick={() =>\n\t\t\t\t\t\t\tactiveId !== null && onAllow(activeId, remember)\n\t\t\t\t\t\t}\n\t\t\t\t\t\tdata-testid=\"plugin-download-allow\"\n\t\t\t\t\t>\n\t\t\t\t\t\t{t(\"pluginDownload.allow\", undefined)}\n\t\t\t\t\t</Button>\n\t\t\t\t</div>\n\t\t\t}\n\t\t>\n\t\t\t{entry === null ? null : itemCount <= 1 ? (\n\t\t\t\t<ConsentBody\n\t\t\t\t\tentry={entry.items[0]!}\n\t\t\t\t\tremember={remember}\n\t\t\t\t\tonRememberChange={setRemember}\n\t\t\t\t\tformatBytes={formatBytes}\n\t\t\t\t/>\n\t\t\t) : (\n\t\t\t\t<ConsentBatch\n\t\t\t\t\titems={entry.items}\n\t\t\t\t\tremember={remember}\n\t\t\t\t\tonRememberChange={setRemember}\n\t\t\t\t/>\n\t\t\t)}\n\t\t</AppDialog>\n\t)\n}\n\nfunction ConsentBody(props: {\n\treadonly entry: PluginConsentItem\n\treadonly remember: boolean\n\treadonly onRememberChange: (value: boolean) => void\n\treadonly formatBytes: (bytes: number) => string\n}) {\n\tconst { entry, remember, onRememberChange, formatBytes } = props\n\tconst { t } = useTranslation(\"ui\", { useSuspense: false })\n\treturn (\n\t\t<div className=\"flex flex-col gap-4\">\n\t\t\t<ConsentUrlField label={t(\"pluginDownload.urlLabel\", undefined)} url={entry.url} />\n\t\t\t<div className=\"flex flex-col gap-1.5\">\n\t\t\t\t<span className=\"text-xs tracking-label text-muted-foreground uppercase\">\n\t\t\t\t\t{t(\"pluginDownload.destLabel\", undefined)}\n\t\t\t\t</span>\n\t\t\t\t<code\n\t\t\t\t\tclassName=\"text-xs text-secondary-foreground break-all select-all\"\n\t\t\t\t\tdata-testid=\"plugin-download-dest\"\n\t\t\t\t>\n\t\t\t\t\t{entry.dest}\n\t\t\t\t</code>\n\t\t\t</div>\n\t\t\t<div className=\"text-xs text-muted-foreground\">\n\t\t\t\t{t(\"pluginDownload.sizeLabel\", undefined)}:{\" \"}\n\t\t\t\t{entry.sizeBytes === undefined\n\t\t\t\t\t? t(\"pluginDownload.unknownSize\", undefined)\n\t\t\t\t\t: formatBytes(entry.sizeBytes)}\n\t\t\t</div>\n\t\t\t{entry.reason !== undefined && entry.reason.length > 0 ? (\n\t\t\t\t<p className=\"text-xs text-secondary-foreground italic\">\n\t\t\t\t\t{entry.reason}\n\t\t\t\t</p>\n\t\t\t) : null}\n\t\t\t<RememberRow remember={remember} onRememberChange={onRememberChange} />\n\t\t</div>\n\t)\n}\n\n/** Batch layout: one row per download, URL verbatim + dest + reason. */\nfunction ConsentBatch(props: {\n\treadonly items: readonly PluginConsentItem[]\n\treadonly remember: boolean\n\treadonly onRememberChange: (value: boolean) => void\n}) {\n\tconst { items, remember, onRememberChange } = props\n\tconst { t } = useTranslation(\"ui\", { useSuspense: false })\n\treturn (\n\t\t<div className=\"flex flex-col gap-4\">\n\t\t\t<ul className=\"flex max-h-52 flex-col gap-3 overflow-y-auto pr-1\">\n\t\t\t\t{items.map((item, index) => (\n\t\t\t\t\t<li\n\t\t\t\t\t\tkey={`${item.url}#${item.dest}`}\n\t\t\t\t\t\tclassName=\"flex flex-col gap-1 rounded-md border border-border px-2.5 py-2\"\n\t\t\t\t\t\tdata-testid={`plugin-download-item-${index}`}\n\t\t\t\t\t>\n\t\t\t\t\t\t<ConsentUrlField\n\t\t\t\t\t\t\tlabel={t(\"pluginDownload.itemLabel\", {\n\t\t\t\t\t\t\t\tindex: index + 1,\n\t\t\t\t\t\t\t})}\n\t\t\t\t\t\t\turl={item.url}\n\t\t\t\t\t\t\ttestId={`plugin-download-url-${index}`}\n\t\t\t\t\t\t/>\n\t\t\t\t\t\t<code\n\t\t\t\t\t\t\tclassName=\"text-xs text-secondary-foreground break-all select-all\"\n\t\t\t\t\t\t\tdata-testid={`plugin-download-dest-${index}`}\n\t\t\t\t\t\t>\n\t\t\t\t\t\t\t{item.dest}\n\t\t\t\t\t\t</code>\n\t\t\t\t\t\t{item.reason !== undefined && item.reason.length > 0 ? (\n\t\t\t\t\t\t\t<p className=\"text-xs text-secondary-foreground italic\">\n\t\t\t\t\t\t\t\t{item.reason}\n\t\t\t\t\t\t\t</p>\n\t\t\t\t\t\t) : null}\n\t\t\t\t\t</li>\n\t\t\t\t))}\n\t\t\t</ul>\n\t\t\t<p className=\"text-xs text-muted-foreground\">\n\t\t\t\t{t(\"pluginDownload.batchNote\", { count: items.length })}\n\t\t\t</p>\n\t\t\t<RememberRow remember={remember} onRememberChange={onRememberChange} />\n\t\t</div>\n\t)\n}\n\nfunction ConsentUrlField(props: {\n\treadonly label: string\n\treadonly url: string\n\treadonly testId?: string\n}) {\n\treturn (\n\t\t<div className=\"flex min-w-0 flex-col gap-1\">\n\t\t\t<span className=\"text-xs tracking-label text-muted-foreground uppercase\">\n\t\t\t\t{props.label}\n\t\t\t</span>\n\t\t\t<code\n\t\t\t\tclassName=\"rounded-sm bg-muted px-2 py-1.5 text-xs text-secondary-foreground break-all select-all\"\n\t\t\t\tdata-testid={props.testId ?? \"plugin-download-url\"}\n\t\t\t>\n\t\t\t\t{props.url}\n\t\t\t</code>\n\t\t</div>\n\t)\n}\n\nfunction RememberRow(props: {\n\treadonly remember: boolean\n\treadonly onRememberChange: (value: boolean) => void\n}) {\n\tconst { remember, onRememberChange } = props\n\tconst { t } = useTranslation(\"ui\", { useSuspense: false })\n\treturn (\n\t\t<div className=\"flex items-center gap-2 border-t pt-3\">\n\t\t\t<Checkbox\n\t\t\t\tchecked={remember}\n\t\t\t\tonCheckedChange={(checked) => onRememberChange(checked === true)}\n\t\t\t\tid=\"plugin-download-remember\"\n\t\t\t/>\n\t\t\t<label\n\t\t\t\thtmlFor=\"plugin-download-remember\"\n\t\t\t\tclassName=\"text-xs text-secondary-foreground select-none\"\n\t\t\t>\n\t\t\t\t{t(\"pluginDownload.remember\", undefined)}\n\t\t\t</label>\n\t\t</div>\n\t)\n}\n"]}
|
|
@@ -9,7 +9,15 @@ import { ReactNode } from 'react';
|
|
|
9
9
|
* the body.
|
|
10
10
|
*/
|
|
11
11
|
type DialogFooterActionsValue = {
|
|
12
|
+
/** The right-aligned primary action slot (after the dialog's own footer). */
|
|
12
13
|
readonly setFooterActions: (node: ReactNode | null) => void;
|
|
14
|
+
/**
|
|
15
|
+
* The left-edge function key slot, rendered before the dialog's footer so
|
|
16
|
+
* a destructive/secondary function key can sit at the left edge while
|
|
17
|
+
* cancel and the primary action stay right-aligned (DESIGN.md — three
|
|
18
|
+
* button footers).
|
|
19
|
+
*/
|
|
20
|
+
readonly setLeadingActions: (node: ReactNode | null) => void;
|
|
13
21
|
};
|
|
14
22
|
declare const DialogFooterActionsContext: React.Context<DialogFooterActionsValue | null>;
|
|
15
23
|
/** The nearest dialog's footer action slot, or null outside a dialog. */
|
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
export { DialogFooterActionsContext, useDialogFooterActions } from '../chunk-
|
|
1
|
+
export { DialogFooterActionsContext, useDialogFooterActions } from '../chunk-KRUCMXDD.js';
|
|
2
2
|
//# sourceMappingURL=use-dialog-footer-actions.js.map
|
|
3
3
|
//# sourceMappingURL=use-dialog-footer-actions.js.map
|
package/dist/index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
export { AppDialog } from './chunk-
|
|
2
|
-
import './chunk-
|
|
1
|
+
export { AppDialog } from './chunk-6JMORBVL.js';
|
|
2
|
+
import './chunk-KRUCMXDD.js';
|
|
3
3
|
import './chunk-KOFWEPPB.js';
|
|
4
4
|
export { setNavigationResolver, useMobileBackToClose } from './chunk-TDDHQZ62.js';
|
|
5
5
|
import './chunk-QGMDWGJT.js';
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hoardodile/ui",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.11",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"description": "shadcn/ui component library for hoardodile plugin iframes.",
|
|
6
6
|
"keywords": [
|
|
@@ -83,8 +83,8 @@
|
|
|
83
83
|
"react-image-crop": "^11.1.2",
|
|
84
84
|
"react-use": "^17.6.1",
|
|
85
85
|
"tailwind-merge": "^3.6.0",
|
|
86
|
-
"@hoardodile/
|
|
87
|
-
"@hoardodile/
|
|
86
|
+
"@hoardodile/i18n": "0.1.11",
|
|
87
|
+
"@hoardodile/sdk-types": "0.1.11"
|
|
88
88
|
},
|
|
89
89
|
"peerDependencies": {
|
|
90
90
|
"i18next": "^26.4.0",
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { screen } from "@testing-library/react"
|
|
2
|
+
import { describe, expect, it } from "vitest"
|
|
3
|
+
import { renderWithI18n } from "../test/i18n"
|
|
4
|
+
import {
|
|
5
|
+
AppDialog,
|
|
6
|
+
DialogFooterActions,
|
|
7
|
+
DialogFooterLeadingActions,
|
|
8
|
+
} from "./app-dialog"
|
|
9
|
+
|
|
10
|
+
const FOLLOWS = Node.DOCUMENT_POSITION_FOLLOWING
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* The leading (left-edge) footer action placement follows DESIGN.md:
|
|
14
|
+
* with a primary action the bar splits ([remove] [cancel] [save]); with no
|
|
15
|
+
* primary it never splits ([cancel] [remove]).
|
|
16
|
+
*/
|
|
17
|
+
describe("AppDialog footer actions", () => {
|
|
18
|
+
it("places the leading action before Cancel when a primary action is present", async () => {
|
|
19
|
+
renderWithI18n(
|
|
20
|
+
<AppDialog
|
|
21
|
+
open
|
|
22
|
+
title="Dialog"
|
|
23
|
+
onOpenChange={() => {}}
|
|
24
|
+
footer={
|
|
25
|
+
<button type="button" data-testid="cancel">
|
|
26
|
+
Cancel
|
|
27
|
+
</button>
|
|
28
|
+
}
|
|
29
|
+
>
|
|
30
|
+
<DialogFooterActions>
|
|
31
|
+
<button type="button" data-testid="save">
|
|
32
|
+
Save
|
|
33
|
+
</button>
|
|
34
|
+
</DialogFooterActions>
|
|
35
|
+
<DialogFooterLeadingActions>
|
|
36
|
+
<button type="button" data-testid="remove">
|
|
37
|
+
Remove
|
|
38
|
+
</button>
|
|
39
|
+
</DialogFooterLeadingActions>
|
|
40
|
+
</AppDialog>,
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
const remove = await screen.findByTestId("remove")
|
|
44
|
+
const cancel = screen.getByTestId("cancel")
|
|
45
|
+
// Remove documents before Cancel ⇒ remove is at the left edge.
|
|
46
|
+
expect(remove.compareDocumentPosition(cancel) & FOLLOWS).toBe(FOLLOWS)
|
|
47
|
+
})
|
|
48
|
+
|
|
49
|
+
it("places the leading action after Cancel when there is no primary action", async () => {
|
|
50
|
+
renderWithI18n(
|
|
51
|
+
<AppDialog
|
|
52
|
+
open
|
|
53
|
+
title="Dialog"
|
|
54
|
+
onOpenChange={() => {}}
|
|
55
|
+
footer={
|
|
56
|
+
<button type="button" data-testid="cancel">
|
|
57
|
+
Cancel
|
|
58
|
+
</button>
|
|
59
|
+
}
|
|
60
|
+
>
|
|
61
|
+
<DialogFooterLeadingActions>
|
|
62
|
+
<button type="button" data-testid="remove">
|
|
63
|
+
Remove
|
|
64
|
+
</button>
|
|
65
|
+
</DialogFooterLeadingActions>
|
|
66
|
+
</AppDialog>,
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
const remove = await screen.findByTestId("remove")
|
|
70
|
+
const cancel = screen.getByTestId("cancel")
|
|
71
|
+
// Cancel documents before Remove ⇒ remove sits to the right of Cancel.
|
|
72
|
+
expect(cancel.compareDocumentPosition(remove) & FOLLOWS).toBe(FOLLOWS)
|
|
73
|
+
})
|
|
74
|
+
})
|
|
@@ -53,6 +53,26 @@ export function DialogFooterActions({ children }: { children: ReactNode }) {
|
|
|
53
53
|
return null
|
|
54
54
|
}
|
|
55
55
|
|
|
56
|
+
/**
|
|
57
|
+
* Declarative left-edge footer action: contributes the node to the
|
|
58
|
+
* surrounding dialog's footer front, pushed to the left edge (cancel and
|
|
59
|
+
* the primary action stay right-aligned — DESIGN.md three-button footers).
|
|
60
|
+
* Renders nothing outside a dialog.
|
|
61
|
+
*/
|
|
62
|
+
export function DialogFooterLeadingActions({
|
|
63
|
+
children,
|
|
64
|
+
}: {
|
|
65
|
+
children: ReactNode
|
|
66
|
+
}) {
|
|
67
|
+
const slot = useDialogFooterActions()
|
|
68
|
+
useEffect(() => {
|
|
69
|
+
if (slot === null) return
|
|
70
|
+
slot.setLeadingActions(children)
|
|
71
|
+
return () => slot.setLeadingActions(null)
|
|
72
|
+
}, [slot, children])
|
|
73
|
+
return null
|
|
74
|
+
}
|
|
75
|
+
|
|
56
76
|
export type AppDialogProps = Readonly<{
|
|
57
77
|
open: boolean
|
|
58
78
|
onOpenChange: (open: boolean) => void
|
|
@@ -122,9 +142,13 @@ export function AppDialog(props: AppDialogProps) {
|
|
|
122
142
|
// Footer actions contributed by body panels (DialogFooterActions):
|
|
123
143
|
// appended after the dialog's own footer, reset when the dialog closes.
|
|
124
144
|
const [footerActions, setFooterActions] = useState<ReactNode>(null)
|
|
145
|
+
// Left-edge function key (DialogFooterLeadingActions): rendered at the
|
|
146
|
+
// footer's left while cancel + the primary action stay right-aligned
|
|
147
|
+
// (DESIGN.md — three button footers). Reset when the dialog closes.
|
|
148
|
+
const [leadingActions, setLeadingActions] = useState<ReactNode>(null)
|
|
125
149
|
const actionsValue = useMemo(
|
|
126
|
-
() => ({ setFooterActions }),
|
|
127
|
-
[setFooterActions],
|
|
150
|
+
() => ({ setFooterActions, setLeadingActions }),
|
|
151
|
+
[setFooterActions, setLeadingActions],
|
|
128
152
|
)
|
|
129
153
|
|
|
130
154
|
return (
|
|
@@ -175,10 +199,24 @@ export function AppDialog(props: AppDialogProps) {
|
|
|
175
199
|
</DialogFooterActionsContext.Provider>
|
|
176
200
|
</DialogBody>
|
|
177
201
|
{(footer !== undefined && footer !== null) ||
|
|
178
|
-
footerActions !== null
|
|
202
|
+
footerActions !== null ||
|
|
203
|
+
leadingActions !== null ? (
|
|
204
|
+
// Footer-action placement (DESIGN.md — dialog anatomy):
|
|
205
|
+
// with a primary action the bar splits — the leading
|
|
206
|
+
// function key sits at the left edge while cancel + the
|
|
207
|
+
// primary stay right-aligned ([remove] [cancel] [save]);
|
|
208
|
+
// without a primary the bar never splits — cancel leads
|
|
209
|
+
// and the function key holds the right edge ([cancel]
|
|
210
|
+
// [remove]).
|
|
179
211
|
<DialogFooter flush={flush}>
|
|
212
|
+
{leadingActions !== null && footerActions !== null ? (
|
|
213
|
+
<div className="mr-auto">{leadingActions}</div>
|
|
214
|
+
) : null}
|
|
180
215
|
{footer}
|
|
181
216
|
{footerActions}
|
|
217
|
+
{leadingActions !== null && footerActions === null ? (
|
|
218
|
+
<>{leadingActions}</>
|
|
219
|
+
) : null}
|
|
182
220
|
</DialogFooter>
|
|
183
221
|
) : null}
|
|
184
222
|
</DialogContent>
|
|
@@ -141,17 +141,35 @@ describe("ImageCropPanel", () => {
|
|
|
141
141
|
})
|
|
142
142
|
|
|
143
143
|
it("renders the action button as an inline panel row outside a dialog", () => {
|
|
144
|
-
const { container } = renderWithI18n(
|
|
145
|
-
<ImageCropPanel onClear={vi.fn()} onSave={vi.fn()} />,
|
|
146
|
-
)
|
|
144
|
+
const { container } = renderWithI18n(<ImageCropPanel onSave={vi.fn()} />)
|
|
147
145
|
|
|
148
146
|
expect(screen.getByTestId("image-crop-frame")).toBeInTheDocument()
|
|
149
|
-
// No image selected: the
|
|
150
|
-
|
|
151
|
-
|
|
147
|
+
// No image selected: the primary stays "Save" but is disabled — it is
|
|
148
|
+
// never a "Remove" action (removal is a dedicated danger button).
|
|
149
|
+
const save = screen.getByTestId("image-crop-save")
|
|
150
|
+
expect(save).toHaveTextContent("Save")
|
|
151
|
+
expect(save).toBeDisabled()
|
|
152
|
+
// The reselect ("re-upload") affordance only appears once an image is
|
|
153
|
+
// selected.
|
|
154
|
+
expect(screen.queryByTestId("image-crop-reselect")).not.toBeInTheDocument()
|
|
152
155
|
expect(container.querySelector('[data-slot="dialog-footer"]')).toBeNull()
|
|
153
156
|
})
|
|
154
157
|
|
|
158
|
+
it("keeps the primary Save enabled and labeled 'Save' when an image is present", async () => {
|
|
159
|
+
renderWithI18n(<ImageCropPanel initialSrc={TALL_IMAGE_SRC} onSave={vi.fn()} />)
|
|
160
|
+
const save = await screen.findByTestId("image-crop-save")
|
|
161
|
+
expect(save).toHaveTextContent("Save")
|
|
162
|
+
expect(save).toBeEnabled()
|
|
163
|
+
})
|
|
164
|
+
|
|
165
|
+
it("renders the reselect button above the container when an image is selected", async () => {
|
|
166
|
+
renderWithI18n(<ImageCropPanel initialSrc={TALL_IMAGE_SRC} onSave={vi.fn()} />)
|
|
167
|
+
expect(await screen.findByTestId("image-crop-reselect")).toBeInTheDocument()
|
|
168
|
+
expect(screen.getByTestId("image-crop-reselect")).toHaveTextContent(
|
|
169
|
+
"Re-upload",
|
|
170
|
+
)
|
|
171
|
+
})
|
|
172
|
+
|
|
155
173
|
it("hides the action button when hideActionButton is set", async () => {
|
|
156
174
|
renderWithI18n(
|
|
157
175
|
<ImageCropPanel
|
|
@@ -29,12 +29,6 @@ export type ImageCropPanelProps = {
|
|
|
29
29
|
*/
|
|
30
30
|
readonly onSave: (cropped: CroppedImage) => Promise<unknown>
|
|
31
31
|
readonly onSaved?: () => void
|
|
32
|
-
/**
|
|
33
|
-
* Called when the user clicks the action button but no image has been
|
|
34
|
-
* selected. The parent (e.g. {@link ImageEditPanel}) can treat this as a
|
|
35
|
-
* request to remove the existing image.
|
|
36
|
-
*/
|
|
37
|
-
readonly onClear?: () => void
|
|
38
32
|
/**
|
|
39
33
|
* When set (e.g. canvas snapshot object URL), skips the empty picker and
|
|
40
34
|
* shows the cropper immediately.
|
|
@@ -98,7 +92,6 @@ export function ImageCropPanel(props: ImageCropPanelProps) {
|
|
|
98
92
|
previewHeight: previewHeightProp,
|
|
99
93
|
onSave,
|
|
100
94
|
onSaved,
|
|
101
|
-
onClear,
|
|
102
95
|
initialSrc,
|
|
103
96
|
allowChangeSource = true,
|
|
104
97
|
onSavingChange,
|
|
@@ -237,10 +230,7 @@ export function ImageCropPanel(props: ImageCropPanelProps) {
|
|
|
237
230
|
|
|
238
231
|
async function handleSave() {
|
|
239
232
|
const render = renderRef.current
|
|
240
|
-
if (render === undefined)
|
|
241
|
-
onClear?.()
|
|
242
|
-
return
|
|
243
|
-
}
|
|
233
|
+
if (render === undefined) return
|
|
244
234
|
setSaving(true)
|
|
245
235
|
onSavingChange?.(true)
|
|
246
236
|
try {
|
|
@@ -266,14 +256,10 @@ export function ImageCropPanel(props: ImageCropPanelProps) {
|
|
|
266
256
|
<Button
|
|
267
257
|
type="button"
|
|
268
258
|
onClick={handleSave}
|
|
269
|
-
disabled={saving}
|
|
259
|
+
disabled={saving || imageSrc === undefined}
|
|
270
260
|
data-testid="image-crop-save"
|
|
271
261
|
>
|
|
272
|
-
{saving
|
|
273
|
-
? t("imageCrop.saving")
|
|
274
|
-
: imageSrc === undefined
|
|
275
|
-
? t("imageCrop.remove")
|
|
276
|
-
: t("imageCrop.save")}
|
|
262
|
+
{saving ? t("imageCrop.saving") : t("imageCrop.save")}
|
|
277
263
|
</Button>
|
|
278
264
|
)
|
|
279
265
|
|
|
@@ -290,6 +276,19 @@ export function ImageCropPanel(props: ImageCropPanelProps) {
|
|
|
290
276
|
/>
|
|
291
277
|
) : null}
|
|
292
278
|
|
|
279
|
+
{imageSrc !== undefined && allowChangeSource ? (
|
|
280
|
+
<Button
|
|
281
|
+
type="button"
|
|
282
|
+
variant="secondary"
|
|
283
|
+
onClick={handleReselect}
|
|
284
|
+
className="self-center"
|
|
285
|
+
data-testid="image-crop-reselect"
|
|
286
|
+
>
|
|
287
|
+
<RefreshCircle className="h-4 w-4" />
|
|
288
|
+
{t("imageCrop.reselect")}
|
|
289
|
+
</Button>
|
|
290
|
+
) : null}
|
|
291
|
+
|
|
293
292
|
<div
|
|
294
293
|
className={cn(
|
|
295
294
|
"flex flex-col items-center gap-6 py-2 sm:flex-row sm:items-center sm:justify-center",
|
|
@@ -390,18 +389,6 @@ export function ImageCropPanel(props: ImageCropPanelProps) {
|
|
|
390
389
|
/>
|
|
391
390
|
</div>
|
|
392
391
|
)}
|
|
393
|
-
{imageSrc !== undefined && allowChangeSource ? (
|
|
394
|
-
<Button
|
|
395
|
-
type="button"
|
|
396
|
-
size="sm"
|
|
397
|
-
variant="ghost"
|
|
398
|
-
onClick={handleReselect}
|
|
399
|
-
className="text-muted-foreground text-xs"
|
|
400
|
-
>
|
|
401
|
-
<RefreshCircle className="mr-1 h-3.5 w-3.5" />
|
|
402
|
-
{t("imageCrop.reselect")}
|
|
403
|
-
</Button>
|
|
404
|
-
) : null}
|
|
405
392
|
</div>
|
|
406
393
|
|
|
407
394
|
{previewVisible ? (
|
|
@@ -66,7 +66,7 @@ export type PluginDownloadConsentDialogProps = {
|
|
|
66
66
|
* and the plugin's stated reason, with Allow / Deny and a session-remember
|
|
67
67
|
* checkbox. A batch is one dialog listing all items.
|
|
68
68
|
*
|
|
69
|
-
* Design contract (hd-
|
|
69
|
+
* Design contract (hd-design): card + hairline + `--radius-2xl`
|
|
70
70
|
* + `--shadow-dialog`, footer parted by an inset hairline, focus on the
|
|
71
71
|
* dialog container; the danger stays in copy and actions, never in color.
|
|
72
72
|
*/
|
|
@@ -9,7 +9,15 @@ import { createContext, type ReactNode, useContext } from "react"
|
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
11
|
type DialogFooterActionsValue = {
|
|
12
|
+
/** The right-aligned primary action slot (after the dialog's own footer). */
|
|
12
13
|
readonly setFooterActions: (node: ReactNode | null) => void
|
|
14
|
+
/**
|
|
15
|
+
* The left-edge function key slot, rendered before the dialog's footer so
|
|
16
|
+
* a destructive/secondary function key can sit at the left edge while
|
|
17
|
+
* cancel and the primary action stay right-aligned (DESIGN.md — three
|
|
18
|
+
* button footers).
|
|
19
|
+
*/
|
|
20
|
+
readonly setLeadingActions: (node: ReactNode | null) => void
|
|
13
21
|
}
|
|
14
22
|
|
|
15
23
|
const DialogFooterActionsContext =
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/components/app-dialog.tsx"],"names":[],"mappings":"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","file":"chunk-2BVZEYNW.js","sourcesContent":["import {\n\tDialog,\n\tDialogBody,\n\tDialogContent,\n\tDialogDescription,\n\tDialogFooter,\n\tDialogHeader,\n\tDialogTitle,\n} from \"./dialog\"\nimport {\n\tDialogFooterActionsContext,\n\tuseDialogFooterActions,\n} from \"@hoardodile/ui/hooks/use-dialog-footer-actions\"\nimport { cn } from \"@hoardodile/ui/lib/utils\"\nimport {\n\ttype ReactNode,\n\tuseEffect,\n\tuseMemo,\n\tuseRef,\n\tuseState,\n} from \"react\"\n\n/** Dialog width tiers: the 320–480 narrow slot plus\n the wide working tiers — edit hubs, pickers and selectors get\n real estate; confirmations never leave the narrow slot. */\nexport type AppDialogSize = \"sm\" | \"md\" | \"lg\" | \"xl\" | \"2xl\"\n\nconst sizeClasses: Record<AppDialogSize, string> = {\n\tsm: \"sm:max-w-sm\",\n\tmd: \"sm:max-w-md\",\n\tlg: \"sm:max-w-2xl\",\n\txl: \"sm:max-w-3xl\",\n\t\"2xl\": \"sm:max-w-4xl\",\n}\n\nexport { useDialogFooterActions }\n\n/**\n * Declarative footer action: renders nothing in the body and contributes\n * the node to the surrounding dialog's footer (appended after the\n * dialog's own `footer` prop). Panels embed their primary action this way\n * so action buttons live in the footer, never in the body. Outside a\n * dialog (sheets, pickers) it renders nothing — callers keep their inline\n * action bar as the fallback.\n */\nexport function DialogFooterActions({ children }: { children: ReactNode }) {\n\tconst slot = useDialogFooterActions()\n\tuseEffect(() => {\n\t\tif (slot === null) return\n\t\tslot.setFooterActions(children)\n\t\treturn () => slot.setFooterActions(null)\n\t}, [slot, children])\n\treturn null\n}\n\nexport type AppDialogProps = Readonly<{\n\topen: boolean\n\tonOpenChange: (open: boolean) => void\n\ttitle: ReactNode\n\t/** Uppercase section marker above the title (feature dialogs). */\n\teyebrow?: ReactNode\n\t/** Leading icon tile in the title row (feature dialogs). */\n\ticon?: ReactNode\n\t/** Danger register — the title ink takes the destructive hue; the\n\t danger itself stays in copy and the confirm button. */\n\tdanger?: boolean\n\t/** Width tier; defaults to `md` (the narrow form slot). */\n\tsize?: AppDialogSize\n\t/**\n\t * Drops the body padding for workspace bodies that own their layout\n\t * edge to edge (the two-pane editors); the header keeps its padding\n\t * and the footer becomes a plain bottom bar.\n\t */\n\tflush?: boolean\n\tdescription?: ReactNode\n\tchildren: ReactNode\n\t/** Leading footer actions; body panels may append theirs via\n\t {@link DialogFooterActions}. */\n\tfooter?: ReactNode\n\tcontentClassName?: string\n\t/** Passed to dialog overlay (e.g. disable backdrop blur over WebGL). */\n\toverlayClassName?: string\n\t/** Fade-only motion — lighter than default slide/zoom when over heavy surfaces. */\n\tcontentMotion?: \"default\" | \"minimal\"\n\t/** Defaults to true. Set to false to honour native auto-focus (rare). */\n\tsuppressAutoFocus?: boolean\n\tcontentTestId?: string\n}>\n\n/**\n * Standard dialog shell. Wires up `Dialog`/`DialogContent`/`DialogHeader`/\n * `DialogTitle` and an optional eyebrow, icon, description and footer so\n * callers stop re-stating the boilerplate. Use {@link ConfirmDialog} for\n * the common \"cancel + primary action\" pattern.\n */\nexport function AppDialog(props: AppDialogProps) {\n\tconst {\n\t\topen,\n\t\tonOpenChange,\n\t\ttitle,\n\t\teyebrow,\n\t\ticon,\n\t\tdanger,\n\t\tsize,\n\t\tflush,\n\t\tdescription,\n\t\tchildren,\n\t\tfooter,\n\t\tcontentClassName,\n\t\toverlayClassName,\n\t\tcontentMotion,\n\t\tsuppressAutoFocus = true,\n\t\tcontentTestId,\n\t} = props\n\t// Focusing the first focusable element on open would steal the caret\n\t// from the page underneath, jump scroll position, and on iOS\n\t// occasionally pop the soft keyboard before the user even reaches an\n\t// input. Routing focus to the dialog container itself keeps the\n\t// dialog accessible without those side effects.\n\tconst contentRef = useRef<HTMLDivElement | null>(null)\n\n\t// Footer actions contributed by body panels (DialogFooterActions):\n\t// appended after the dialog's own footer, reset when the dialog closes.\n\tconst [footerActions, setFooterActions] = useState<ReactNode>(null)\n\tconst actionsValue = useMemo(\n\t\t() => ({ setFooterActions }),\n\t\t[setFooterActions],\n\t)\n\n\treturn (\n\t\t<Dialog open={open} onOpenChange={onOpenChange}>\n\t\t\t<DialogContent\n\t\t\t\tref={contentRef}\n\t\t\t\tclassName={cn(\n\t\t\t\t\tsize !== undefined && sizeClasses[size],\n\t\t\t\t\tflush && \"p-0\",\n\t\t\t\t\tcontentClassName,\n\t\t\t\t)}\n\t\t\t\toverlayClassName={overlayClassName}\n\t\t\t\tcontentMotion={contentMotion}\n\t\t\t\tinitialFocus={suppressAutoFocus ? contentRef : undefined}\n\t\t\t\tdata-testid={contentTestId}\n\t\t\t>\n\t\t\t\t<DialogHeader className={flush ? \"gap-4\" : undefined}>\n\t\t\t\t\t{eyebrow !== undefined ? (\n\t\t\t\t\t\t<span\n\t\t\t\t\t\t\tclassName={cn(\n\t\t\t\t\t\t\t\t\"text-tiny font-semibold tracking-label uppercase\",\n\t\t\t\t\t\t\t\tdanger ? \"text-destructive\" : \"text-muted-foreground\",\n\t\t\t\t\t\t\t)}\n\t\t\t\t\t\t>\n\t\t\t\t\t\t\t{eyebrow}\n\t\t\t\t\t\t</span>\n\t\t\t\t\t) : null}\n\t\t\t\t\t<div className=\"flex min-w-0 items-center gap-3 pr-6\">\n\t\t\t\t\t\t{icon}\n\t\t\t\t\t\t<DialogTitle\n\t\t\t\t\t\t\tclassName={cn(\"truncate\", danger && \"text-destructive\")}\n\t\t\t\t\t\t>\n\t\t\t\t\t\t\t{title}\n\t\t\t\t\t\t</DialogTitle>\n\t\t\t\t\t</div>\n\t\t\t\t\t{description !== undefined ? (\n\t\t\t\t\t\t<DialogDescription>{description}</DialogDescription>\n\t\t\t\t\t) : null}\n\t\t\t\t</DialogHeader>\n\t\t\t\t{/* The header, body and footer must stay direct children of\n\t\t\t\t DialogContent — its arrangement logic recognises them by\n\t\t\t\t type, and a wrapper in between would re-wrap the body in a\n\t\t\t\t second DialogBody. The footer-actions context therefore\n\t\t\t\t wraps only the body's own content. */}\n\t\t\t\t<DialogBody className={flush ? \"p-0\" : undefined}>\n\t\t\t\t\t<DialogFooterActionsContext.Provider value={actionsValue}>\n\t\t\t\t\t\t{children}\n\t\t\t\t\t</DialogFooterActionsContext.Provider>\n\t\t\t\t</DialogBody>\n\t\t\t\t{(footer !== undefined && footer !== null) ||\n\t\t\t\tfooterActions !== null ? (\n\t\t\t\t\t<DialogFooter flush={flush}>\n\t\t\t\t\t\t{footer}\n\t\t\t\t\t\t{footerActions}\n\t\t\t\t\t</DialogFooter>\n\t\t\t\t) : null}\n\t\t\t</DialogContent>\n\t\t</Dialog>\n\t)\n}\n"]}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/hooks/use-dialog-footer-actions.ts"],"names":[],"mappings":";;;AAcA,IAAM,0BAAA,GACL,cAA+C,IAAI;AAGpD,SAAS,sBAAA,GAA0D;AAClE,EAAA,OAAO,WAAW,0BAA0B,CAAA;AAC7C","file":"chunk-6HAIUXNZ.js","sourcesContent":["import { createContext, type ReactNode, useContext } from \"react\"\n\n/**\n * Footer action slot plumbing for `AppDialog` — the context shared\n * between the dialog shell (`@hoardodile/ui/components/app-dialog`) and\n * body panels that contribute their primary action through\n * `DialogFooterActions`, so action buttons land in the footer instead of\n * the body.\n */\n\ntype DialogFooterActionsValue = {\n\treadonly setFooterActions: (node: ReactNode | null) => void\n}\n\nconst DialogFooterActionsContext =\n\tcreateContext<DialogFooterActionsValue | null>(null)\n\n/** The nearest dialog's footer action slot, or null outside a dialog. */\nfunction useDialogFooterActions(): DialogFooterActionsValue | null {\n\treturn useContext(DialogFooterActionsContext)\n}\n\nexport { DialogFooterActionsContext, useDialogFooterActions }\n"]}
|