@molecule/app-ui-react 1.0.2 → 1.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +521 -1
- package/dist/components/ConfirmButton.d.ts +71 -0
- package/dist/components/ConfirmButton.d.ts.map +1 -0
- package/dist/components/ConfirmButton.js +88 -0
- package/dist/components/ConfirmButton.js.map +1 -0
- package/dist/components/ConfirmDialog.d.ts +71 -0
- package/dist/components/ConfirmDialog.d.ts.map +1 -0
- package/dist/components/ConfirmDialog.js +20 -0
- package/dist/components/ConfirmDialog.js.map +1 -0
- package/dist/components/Icon.d.ts.map +1 -1
- package/dist/components/Icon.js +12 -1
- package/dist/components/Icon.js.map +1 -1
- package/dist/components/LoadErrorBanner.d.ts +71 -0
- package/dist/components/LoadErrorBanner.d.ts.map +1 -0
- package/dist/components/LoadErrorBanner.js +31 -0
- package/dist/components/LoadErrorBanner.js.map +1 -0
- package/dist/components/PromptDialog.d.ts +91 -0
- package/dist/components/PromptDialog.d.ts.map +1 -0
- package/dist/components/PromptDialog.js +90 -0
- package/dist/components/PromptDialog.js.map +1 -0
- package/dist/components/ResponsiveAppShell.d.ts +327 -0
- package/dist/components/ResponsiveAppShell.d.ts.map +1 -0
- package/dist/components/ResponsiveAppShell.js +470 -0
- package/dist/components/ResponsiveAppShell.js.map +1 -0
- package/dist/components/UserMenu.d.ts.map +1 -1
- package/dist/components/UserMenu.js +5 -1
- package/dist/components/UserMenu.js.map +1 -1
- package/dist/components/index.d.ts +5 -0
- package/dist/components/index.d.ts.map +1 -1
- package/dist/components/index.js +5 -0
- package/dist/components/index.js.map +1 -1
- package/package.json +2 -2
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
/**
|
|
3
|
+
* ConfirmButton component.
|
|
4
|
+
*
|
|
5
|
+
* @module
|
|
6
|
+
*/
|
|
7
|
+
import { forwardRef, useEffect, useRef, useState } from 'react';
|
|
8
|
+
import { useTranslation } from '@molecule/app-react';
|
|
9
|
+
import { getClassMap } from '@molecule/app-ui';
|
|
10
|
+
/**
|
|
11
|
+
* Two-step destructive-action button: the first click ARMS it ("Confirm?"),
|
|
12
|
+
* the second commits. Auto-disarms after {@link ConfirmButtonProps.disarmSeconds}
|
|
13
|
+
* (default 4s) so an armed-then-abandoned row can't surprise anyone later.
|
|
14
|
+
*
|
|
15
|
+
* Replaces the fleet's hand-rolled arm-confirm pattern (previously
|
|
16
|
+
* reimplemented per app — button flips, dialogs, `window.confirm`, or worse,
|
|
17
|
+
* nothing at all) with one accessible, touch-friendly implementation:
|
|
18
|
+
* the whole button is the target (≥36px via the button CVA), the armed
|
|
19
|
+
* state is announced through `aria-live`, and Escape disarms.
|
|
20
|
+
*
|
|
21
|
+
* @example
|
|
22
|
+
* ```tsx
|
|
23
|
+
* <ConfirmButton
|
|
24
|
+
* label="Delete"
|
|
25
|
+
* confirmLabel="Confirm delete?"
|
|
26
|
+
* onConfirm={() => deleteClient(client.id)}
|
|
27
|
+
* testId="client-delete"
|
|
28
|
+
* />
|
|
29
|
+
* ```
|
|
30
|
+
*/
|
|
31
|
+
export const ConfirmButton = forwardRef(({ onConfirm, label, confirmLabel, pendingLabel, disarmSeconds = 4, disabled, testId, className, color, size, variant, ...rest }, ref) => {
|
|
32
|
+
const cm = getClassMap();
|
|
33
|
+
const { t } = useTranslation();
|
|
34
|
+
const [phase, setPhase] = useState('idle');
|
|
35
|
+
const disarmTimer = useRef(null);
|
|
36
|
+
const disarm = () => {
|
|
37
|
+
if (disarmTimer.current)
|
|
38
|
+
clearTimeout(disarmTimer.current);
|
|
39
|
+
disarmTimer.current = null;
|
|
40
|
+
setPhase('idle');
|
|
41
|
+
};
|
|
42
|
+
useEffect(() => () => {
|
|
43
|
+
if (disarmTimer.current)
|
|
44
|
+
clearTimeout(disarmTimer.current);
|
|
45
|
+
}, []);
|
|
46
|
+
useEffect(() => {
|
|
47
|
+
if (phase !== 'armed' || disarmSeconds <= 0)
|
|
48
|
+
return;
|
|
49
|
+
disarmTimer.current = setTimeout(disarm, disarmSeconds * 1000);
|
|
50
|
+
return () => {
|
|
51
|
+
if (disarmTimer.current)
|
|
52
|
+
clearTimeout(disarmTimer.current);
|
|
53
|
+
};
|
|
54
|
+
}, [phase, disarmSeconds]);
|
|
55
|
+
useEffect(() => {
|
|
56
|
+
if (phase !== 'armed')
|
|
57
|
+
return;
|
|
58
|
+
const onKey = (e) => {
|
|
59
|
+
if (e.key === 'Escape')
|
|
60
|
+
disarm();
|
|
61
|
+
};
|
|
62
|
+
window.addEventListener('keydown', onKey);
|
|
63
|
+
return () => window.removeEventListener('keydown', onKey);
|
|
64
|
+
}, [phase]);
|
|
65
|
+
const handleClick = async () => {
|
|
66
|
+
if (phase === 'idle') {
|
|
67
|
+
setPhase('armed');
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
if (phase === 'pending')
|
|
71
|
+
return;
|
|
72
|
+
setPhase('pending');
|
|
73
|
+
try {
|
|
74
|
+
await onConfirm();
|
|
75
|
+
}
|
|
76
|
+
finally {
|
|
77
|
+
setPhase('idle');
|
|
78
|
+
}
|
|
79
|
+
};
|
|
80
|
+
const visibleLabel = phase === 'pending'
|
|
81
|
+
? (pendingLabel ?? t('confirmButton.pending', {}, { defaultValue: 'Working…' }))
|
|
82
|
+
: phase === 'armed'
|
|
83
|
+
? (confirmLabel ?? t('confirmButton.confirm', {}, { defaultValue: 'Confirm?' }))
|
|
84
|
+
: label;
|
|
85
|
+
return (_jsx("button", { ref: ref, type: "button", onClick: () => void handleClick(), disabled: disabled || phase === 'pending', "aria-live": "polite", "data-state": phase, "data-testid": testId, className: cm.cn(cm.button({ color: phase === 'armed' ? 'error' : (color ?? 'primary'), size, variant }), className), ...rest, children: visibleLabel }));
|
|
86
|
+
});
|
|
87
|
+
ConfirmButton.displayName = 'ConfirmButton';
|
|
88
|
+
//# sourceMappingURL=ConfirmButton.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ConfirmButton.js","sourceRoot":"","sources":["../../src/components/ConfirmButton.tsx"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ConfirmDialog component — the shared "Are you sure?" modal.
|
|
3
|
+
*
|
|
4
|
+
* Promoted from `@molecule/app-danger-zone-react` (2026-09-24) so every app
|
|
5
|
+
* confirms destructive actions through the framework bundle instead of
|
|
6
|
+
* hand-rolling `window.confirm` (broken headless + on touch) or reaching for
|
|
7
|
+
* the danger-zone feature package just to render one dialog. For an INLINE
|
|
8
|
+
* two-click arm/commit affordance use {@link ConfirmButton}; for a dialog
|
|
9
|
+
* that also collects text use {@link PromptDialog}.
|
|
10
|
+
*
|
|
11
|
+
* @example
|
|
12
|
+
* ```tsx
|
|
13
|
+
* import { ConfirmDialog } from '@molecule/app-ui-react'
|
|
14
|
+
*
|
|
15
|
+
* <ConfirmDialog
|
|
16
|
+
* open={confirmOpen}
|
|
17
|
+
* onClose={() => setConfirmOpen(false)}
|
|
18
|
+
* title="Delete vault?"
|
|
19
|
+
* description="This action cannot be undone."
|
|
20
|
+
* confirmLabel={t('vault.delete', {}, { defaultValue: 'Delete' })}
|
|
21
|
+
* onConfirm={handleDelete}
|
|
22
|
+
* />
|
|
23
|
+
* ```
|
|
24
|
+
*
|
|
25
|
+
* @remarks
|
|
26
|
+
* Does NOT track `onConfirm`'s promise and does NOT close itself on confirm:
|
|
27
|
+
* set `loading` yourself while the action runs, then call `onClose()` when it
|
|
28
|
+
* settles — otherwise users can double-fire the destructive action or be left
|
|
29
|
+
* staring at an open dialog. Cancel/Confirm fall back to the `confirm.*`
|
|
30
|
+
* i18n keys (companion bond: `@molecule/app-locales-danger-zone`); pass
|
|
31
|
+
* `confirmLabel`/`cancelLabel` for action-specific wording ("Delete",
|
|
32
|
+
* "Revoke"). `destructive` (default true) only switches the confirm button
|
|
33
|
+
* color. `data-mol-id` for e2e locators goes on YOUR trigger button; the
|
|
34
|
+
* dialog's own buttons are text-labeled ("Cancel"/confirm label) so
|
|
35
|
+
* `getByRole('button', { name })` reaches them.
|
|
36
|
+
*
|
|
37
|
+
* @module
|
|
38
|
+
*/
|
|
39
|
+
import type { JSX, ReactNode } from 'react';
|
|
40
|
+
/** Props for {@link ConfirmDialog}. */
|
|
41
|
+
export interface ConfirmDialogProps {
|
|
42
|
+
/** Whether the dialog is open. */
|
|
43
|
+
open: boolean;
|
|
44
|
+
/** Called when the dialog should close (escape, backdrop, cancel). */
|
|
45
|
+
onClose: () => void;
|
|
46
|
+
/** Title. */
|
|
47
|
+
title: ReactNode;
|
|
48
|
+
/** Body / warning text. */
|
|
49
|
+
description: ReactNode;
|
|
50
|
+
/** Confirm-button label (e.g. "Delete"). */
|
|
51
|
+
confirmLabel?: ReactNode;
|
|
52
|
+
/** Cancel-button label. */
|
|
53
|
+
cancelLabel?: ReactNode;
|
|
54
|
+
/** Called when the user confirms. */
|
|
55
|
+
onConfirm: () => void | Promise<void>;
|
|
56
|
+
/** Is the confirm action destructive? Defaults to true. */
|
|
57
|
+
destructive?: boolean;
|
|
58
|
+
/** Extra body content between description and footer. */
|
|
59
|
+
children?: ReactNode;
|
|
60
|
+
/** Disables both buttons. Set it yourself while your `onConfirm` promise is pending — the dialog does not track it and does not auto-close. */
|
|
61
|
+
loading?: boolean;
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Are-you-sure-style confirmation modal for destructive actions.
|
|
65
|
+
*
|
|
66
|
+
* Use standalone (around delete buttons, revoke tokens, irreversible
|
|
67
|
+
* migrations) or with a DangerZoneSection action.
|
|
68
|
+
* @param props - Component props (see {@link ConfirmDialogProps}).
|
|
69
|
+
*/
|
|
70
|
+
export declare function ConfirmDialog({ open, onClose, title, description, confirmLabel, cancelLabel, onConfirm, destructive, children, loading, }: ConfirmDialogProps): JSX.Element;
|
|
71
|
+
//# sourceMappingURL=ConfirmDialog.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ConfirmDialog.d.ts","sourceRoot":"","sources":["../../src/components/ConfirmDialog.tsx"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAqCG;AAEH,OAAO,KAAK,EAAE,GAAG,EAAE,SAAS,EAAE,MAAM,OAAO,CAAA;AAQ3C,uCAAuC;AACvC,MAAM,WAAW,kBAAkB;IACjC,kCAAkC;IAClC,IAAI,EAAE,OAAO,CAAA;IACb,sEAAsE;IACtE,OAAO,EAAE,MAAM,IAAI,CAAA;IACnB,aAAa;IACb,KAAK,EAAE,SAAS,CAAA;IAChB,2BAA2B;IAC3B,WAAW,EAAE,SAAS,CAAA;IACtB,4CAA4C;IAC5C,YAAY,CAAC,EAAE,SAAS,CAAA;IACxB,2BAA2B;IAC3B,WAAW,CAAC,EAAE,SAAS,CAAA;IACvB,qCAAqC;IACrC,SAAS,EAAE,MAAM,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;IACrC,2DAA2D;IAC3D,WAAW,CAAC,EAAE,OAAO,CAAA;IACrB,yDAAyD;IACzD,QAAQ,CAAC,EAAE,SAAS,CAAA;IACpB,+IAA+I;IAC/I,OAAO,CAAC,EAAE,OAAO,CAAA;CAClB;AAED;;;;;;GAMG;AACH,wBAAgB,aAAa,CAAC,EAC5B,IAAI,EACJ,OAAO,EACP,KAAK,EACL,WAAW,EACX,YAAY,EACZ,WAAW,EACX,SAAS,EACT,WAAkB,EAClB,QAAQ,EACR,OAAO,GACR,EAAE,kBAAkB,GAAG,GAAG,CAAC,OAAO,CA2BlC"}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useTranslation } from '@molecule/app-react';
|
|
3
|
+
import { getClassMap } from '@molecule/app-ui';
|
|
4
|
+
import { Button } from './Button.js';
|
|
5
|
+
import { Modal } from './Modal.js';
|
|
6
|
+
/**
|
|
7
|
+
* Are-you-sure-style confirmation modal for destructive actions.
|
|
8
|
+
*
|
|
9
|
+
* Use standalone (around delete buttons, revoke tokens, irreversible
|
|
10
|
+
* migrations) or with a DangerZoneSection action.
|
|
11
|
+
* @param props - Component props (see {@link ConfirmDialogProps}).
|
|
12
|
+
*/
|
|
13
|
+
export function ConfirmDialog({ open, onClose, title, description, confirmLabel, cancelLabel, onConfirm, destructive = true, children, loading, }) {
|
|
14
|
+
const cm = getClassMap();
|
|
15
|
+
const { t } = useTranslation();
|
|
16
|
+
return (_jsx(Modal, { open: open, onClose: onClose, children: _jsxs("div", { className: cm.stack(3), children: [_jsx("h2", { className: cm.cn(cm.textSize('lg'), cm.fontWeight('bold')), children: title }), _jsx("p", { className: cm.textSize('sm'), children: description }), children, _jsxs("div", { className: cm.flex({ justify: 'end', gap: 'sm' }), children: [_jsx(Button, { variant: "ghost", onClick: onClose, disabled: loading, children: cancelLabel ?? t('confirm.cancel', {}, { defaultValue: 'Cancel' }) }), _jsx(Button, { variant: "solid", color: destructive ? 'error' : 'primary', onClick: () => void onConfirm(), disabled: loading, children: loading
|
|
17
|
+
? '…'
|
|
18
|
+
: (confirmLabel ?? t('confirm.confirm', {}, { defaultValue: 'Confirm' })) })] })] }) }));
|
|
19
|
+
}
|
|
20
|
+
//# sourceMappingURL=ConfirmDialog.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ConfirmDialog.js","sourceRoot":"","sources":["../../src/components/ConfirmDialog.tsx"],"names":[],"mappings":";AAyCA,OAAO,EAAE,cAAc,EAAE,MAAM,qBAAqB,CAAA;AACpD,OAAO,EAAE,WAAW,EAAE,MAAM,kBAAkB,CAAA;AAE9C,OAAO,EAAE,MAAM,EAAE,MAAM,aAAa,CAAA;AACpC,OAAO,EAAE,KAAK,EAAE,MAAM,YAAY,CAAA;AA0BlC;;;;;;GAMG;AACH,MAAM,UAAU,aAAa,CAAC,EAC5B,IAAI,EACJ,OAAO,EACP,KAAK,EACL,WAAW,EACX,YAAY,EACZ,WAAW,EACX,SAAS,EACT,WAAW,GAAG,IAAI,EAClB,QAAQ,EACR,OAAO,GACY;IACnB,MAAM,EAAE,GAAG,WAAW,EAAE,CAAA;IACxB,MAAM,EAAE,CAAC,EAAE,GAAG,cAAc,EAAE,CAAA;IAC9B,OAAO,CACL,KAAC,KAAK,IAAC,IAAI,EAAE,IAAI,EAAE,OAAO,EAAE,OAAO,YACjC,eAAK,SAAS,EAAE,EAAE,CAAC,KAAK,CAAC,CAAC,CAAC,aACzB,aAAI,SAAS,EAAE,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,QAAQ,CAAC,IAAI,CAAC,EAAE,EAAE,CAAC,UAAU,CAAC,MAAM,CAAC,CAAC,YAAG,KAAK,GAAM,EAC5E,YAAG,SAAS,EAAE,EAAE,CAAC,QAAQ,CAAC,IAAI,CAAC,YAAG,WAAW,GAAK,EACjD,QAAQ,EACT,eAAK,SAAS,EAAE,EAAE,CAAC,IAAI,CAAC,EAAE,OAAO,EAAE,KAAK,EAAE,GAAG,EAAE,IAAI,EAAE,CAAC,aACpD,KAAC,MAAM,IAAC,OAAO,EAAC,OAAO,EAAC,OAAO,EAAE,OAAO,EAAE,QAAQ,EAAE,OAAO,YACxD,WAAW,IAAI,CAAC,CAAC,gBAAgB,EAAE,EAAE,EAAE,EAAE,YAAY,EAAE,QAAQ,EAAE,CAAC,GAC5D,EACT,KAAC,MAAM,IACL,OAAO,EAAC,OAAO,EACf,KAAK,EAAE,WAAW,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,SAAS,EACxC,OAAO,EAAE,GAAG,EAAE,CAAC,KAAK,SAAS,EAAE,EAC/B,QAAQ,EAAE,OAAO,YAEhB,OAAO;gCACN,CAAC,CAAC,GAAG;gCACL,CAAC,CAAC,CAAC,YAAY,IAAI,CAAC,CAAC,iBAAiB,EAAE,EAAE,EAAE,EAAE,YAAY,EAAE,SAAS,EAAE,CAAC,CAAC,GACpE,IACL,IACF,GACA,CACT,CAAA;AACH,CAAC"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Icon.d.ts","sourceRoot":"","sources":["../../src/components/Icon.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,EAAE,KAAK,QAAQ,EAAE,MAAM,OAAO,CAAA;AAE5C,OAAO,EAAW,KAAK,QAAQ,EAAE,MAAM,qBAAqB,CAAA;AAE5D;;;;;;;GAOG;AACH,MAAM,WAAW,SAAU,SAAQ,IAAI,CACrC,QAAQ,CAAC,aAAa,CAAC,EACvB,OAAO,GAAG,QAAQ,GAAG,SAAS,GAAG,MAAM,CACxC;IACC;;;;;OAKG;IACH,IAAI,EAAE,QAAQ,CAAA;IACd,sEAAsE;IACtE,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,gDAAgD;IAChD,SAAS,CAAC,EAAE,MAAM,CAAA;CACnB;AAED;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,wBAAgB,IAAI,CAAC,EAAE,IAAI,EAAE,IAAS,EAAE,SAAS,EAAE,GAAG,IAAI,EAAE,EAAE,SAAS,GAAG,KAAK,CAAC,GAAG,CAAC,OAAO,
|
|
1
|
+
{"version":3,"file":"Icon.d.ts","sourceRoot":"","sources":["../../src/components/Icon.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,EAAE,KAAK,QAAQ,EAAE,MAAM,OAAO,CAAA;AAE5C,OAAO,EAAW,KAAK,QAAQ,EAAE,MAAM,qBAAqB,CAAA;AAE5D;;;;;;;GAOG;AACH,MAAM,WAAW,SAAU,SAAQ,IAAI,CACrC,QAAQ,CAAC,aAAa,CAAC,EACvB,OAAO,GAAG,QAAQ,GAAG,SAAS,GAAG,MAAM,CACxC;IACC;;;;;OAKG;IACH,IAAI,EAAE,QAAQ,CAAA;IACd,sEAAsE;IACtE,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,gDAAgD;IAChD,SAAS,CAAC,EAAE,MAAM,CAAA;CACnB;AAED;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,wBAAgB,IAAI,CAAC,EAAE,IAAI,EAAE,IAAS,EAAE,SAAS,EAAE,GAAG,IAAI,EAAE,EAAE,SAAS,GAAG,KAAK,CAAC,GAAG,CAAC,OAAO,CAiF1F"}
|
package/dist/components/Icon.js
CHANGED
|
@@ -24,7 +24,18 @@ import { getIcon } from '@molecule/app-icons';
|
|
|
24
24
|
* @returns An `<svg>` element rendering the named glyph.
|
|
25
25
|
*/
|
|
26
26
|
export function Icon({ name, size = 20, className, ...rest }) {
|
|
27
|
-
|
|
27
|
+
// Graceful degradation: an icon name missing from the bonded set (icon-set
|
|
28
|
+
// version skew, a custom-set with a smaller catalog) must NOT crash the
|
|
29
|
+
// whole app — multi-vendor-marketplace shipped a dead app over this. Warn
|
|
30
|
+
// once per name and render a neutral placeholder circle instead.
|
|
31
|
+
let icon;
|
|
32
|
+
try {
|
|
33
|
+
icon = getIcon(name);
|
|
34
|
+
}
|
|
35
|
+
catch (error) {
|
|
36
|
+
console.warn(`Icon: no bonded glyph for "${name}" — rendering a placeholder.`, error);
|
|
37
|
+
return (_jsx("svg", { width: size, height: size, viewBox: "0 0 20 20", fill: "none", className: className, "aria-hidden": rest['aria-label'] == null && rest['aria-labelledby'] == null && rest.role == null, focusable: "false", ...rest, children: _jsx("circle", { cx: "10", cy: "10", r: "8", fill: "currentColor", opacity: "0.15" }) }));
|
|
38
|
+
}
|
|
28
39
|
const viewBox = icon.viewBox || '0 0 20 20';
|
|
29
40
|
// Placed BEFORE {...rest} so an explicit caller aria-hidden/role wins.
|
|
30
41
|
const decorativeProps = rest['aria-label'] == null && rest['aria-labelledby'] == null && rest.role == null
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Icon.js","sourceRoot":"","sources":["../../src/components/Icon.tsx"],"names":[],"mappings":";AAEA,OAAO,EAAE,OAAO,EAAiB,MAAM,qBAAqB,CAAA;AA2B5D;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,MAAM,UAAU,IAAI,CAAC,EAAE,IAAI,EAAE,IAAI,GAAG,EAAE,EAAE,SAAS,EAAE,GAAG,IAAI,EAAa;IACrE,
|
|
1
|
+
{"version":3,"file":"Icon.js","sourceRoot":"","sources":["../../src/components/Icon.tsx"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* LoadErrorBanner component — the shared "load failed" banner with Retry.
|
|
3
|
+
*
|
|
4
|
+
* The fleet hand-rolled this in dozens of apps (an error line + a Retry
|
|
5
|
+
* button, each drifting in wording, styling, and pending behavior). Use it
|
|
6
|
+
* wherever a page/section fetch FAILS — it pairs with `Skeleton` for the
|
|
7
|
+
* loading state and `EmptyState` for the empty one. For form-submit errors
|
|
8
|
+
* use `<Alert status="error">` instead; this banner is specifically the
|
|
9
|
+
* load-failure-with-retry pattern, and its Retry button owns the
|
|
10
|
+
* pending/disabled state so the caller cannot double-fire it.
|
|
11
|
+
*
|
|
12
|
+
* @example
|
|
13
|
+
* ```tsx
|
|
14
|
+
* import { LoadErrorBanner } from '@molecule/app-ui-react'
|
|
15
|
+
*
|
|
16
|
+
* {error ? (
|
|
17
|
+
* <LoadErrorBanner
|
|
18
|
+
* message={t('decks.loadFailed', {}, { defaultValue: 'Could not load decks.' })}
|
|
19
|
+
* onRetry={load} // return the promise — pending state is automatic
|
|
20
|
+
* />
|
|
21
|
+
* ) : (
|
|
22
|
+
* <DeckList decks={decks} />
|
|
23
|
+
* )}
|
|
24
|
+
* ```
|
|
25
|
+
*
|
|
26
|
+
* @remarks
|
|
27
|
+
* Pass `onRetry` as a promise-returning function; while it is pending the
|
|
28
|
+
* retry button is disabled and shows a localized "Retrying…" label, and a
|
|
29
|
+
* rejection is NOT swallowed silently — the banner stays (callers render
|
|
30
|
+
* persistent failure by keeping `error` set, which keeps this banner
|
|
31
|
+
* mounted). Omit `onRetry` and no retry button renders (display-only
|
|
32
|
+
* failure). `data-mol-id` on the ROOT goes through rest props (fleet
|
|
33
|
+
* locator convention); the retry button carries `retryMolId`
|
|
34
|
+
* (default `load-error-retry`) so e2e can click it without knowing the
|
|
35
|
+
* page. Announced politely (`role="status"` via the inner Alert's
|
|
36
|
+
* `live={false}`) — a load failure on initial render has nothing to
|
|
37
|
+
* interrupt.
|
|
38
|
+
*
|
|
39
|
+
* @module
|
|
40
|
+
*/
|
|
41
|
+
import type { JSX } from 'react';
|
|
42
|
+
import React from 'react';
|
|
43
|
+
/**
|
|
44
|
+
* Props for {@link LoadErrorBanner}. Rest props are forwarded to the root
|
|
45
|
+
* `<div>`, so they are typed as its attributes.
|
|
46
|
+
*/
|
|
47
|
+
export interface LoadErrorBannerProps extends Omit<React.HTMLAttributes<HTMLDivElement>, 'children'> {
|
|
48
|
+
/** The primary error line (e.g. the localized "Could not load X."). */
|
|
49
|
+
message: React.ReactNode;
|
|
50
|
+
/** Secondary line under the message (status code, hint). */
|
|
51
|
+
detail?: React.ReactNode;
|
|
52
|
+
/**
|
|
53
|
+
* Called when Retry is clicked. Return the promise; while pending, the
|
|
54
|
+
* button is disabled and shows "Retrying…". Omit for a display-only
|
|
55
|
+
* banner.
|
|
56
|
+
*/
|
|
57
|
+
onRetry?: () => void | Promise<void>;
|
|
58
|
+
/** Retry-button label. Falls back to a localized "Retry". */
|
|
59
|
+
retryLabel?: React.ReactNode;
|
|
60
|
+
/** `data-mol-id` for the retry button (default `load-error-retry`). */
|
|
61
|
+
retryMolId?: string;
|
|
62
|
+
/** Extra content between message and retry row (e.g. an error code). */
|
|
63
|
+
children?: React.ReactNode;
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Error banner with an owned pending-state Retry button.
|
|
67
|
+
*
|
|
68
|
+
* @param props - Component props (see {@link LoadErrorBannerProps}).
|
|
69
|
+
*/
|
|
70
|
+
export declare function LoadErrorBanner({ message, detail, onRetry, retryLabel, retryMolId, children, ...rest }: LoadErrorBannerProps): JSX.Element;
|
|
71
|
+
//# sourceMappingURL=LoadErrorBanner.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"LoadErrorBanner.d.ts","sourceRoot":"","sources":["../../src/components/LoadErrorBanner.tsx"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAuCG;AAEH,OAAO,KAAK,EAAE,GAAG,EAAE,MAAM,OAAO,CAAA;AAChC,OAAO,KAAmB,MAAM,OAAO,CAAA;AAQvC;;;GAGG;AACH,MAAM,WAAW,oBAAqB,SAAQ,IAAI,CAChD,KAAK,CAAC,cAAc,CAAC,cAAc,CAAC,EACpC,UAAU,CACX;IACC,uEAAuE;IACvE,OAAO,EAAE,KAAK,CAAC,SAAS,CAAA;IACxB,4DAA4D;IAC5D,MAAM,CAAC,EAAE,KAAK,CAAC,SAAS,CAAA;IACxB;;;;OAIG;IACH,OAAO,CAAC,EAAE,MAAM,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;IACpC,6DAA6D;IAC7D,UAAU,CAAC,EAAE,KAAK,CAAC,SAAS,CAAA;IAC5B,uEAAuE;IACvE,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB,wEAAwE;IACxE,QAAQ,CAAC,EAAE,KAAK,CAAC,SAAS,CAAA;CAC3B;AAED;;;;GAIG;AACH,wBAAgB,eAAe,CAAC,EAC9B,OAAO,EACP,MAAM,EACN,OAAO,EACP,UAAU,EACV,UAA+B,EAC/B,QAAQ,EACR,GAAG,IAAI,EACR,EAAE,oBAAoB,GAAG,GAAG,CAAC,OAAO,CA4CpC"}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useState } from 'react';
|
|
3
|
+
import { useTranslation } from '@molecule/app-react';
|
|
4
|
+
import { getClassMap } from '@molecule/app-ui';
|
|
5
|
+
import { Alert } from './Alert.js';
|
|
6
|
+
import { Button } from './Button.js';
|
|
7
|
+
/**
|
|
8
|
+
* Error banner with an owned pending-state Retry button.
|
|
9
|
+
*
|
|
10
|
+
* @param props - Component props (see {@link LoadErrorBannerProps}).
|
|
11
|
+
*/
|
|
12
|
+
export function LoadErrorBanner({ message, detail, onRetry, retryLabel, retryMolId = 'load-error-retry', children, ...rest }) {
|
|
13
|
+
const cm = getClassMap();
|
|
14
|
+
const { t } = useTranslation();
|
|
15
|
+
const [pending, setPending] = useState(false);
|
|
16
|
+
const handleRetry = () => {
|
|
17
|
+
if (!onRetry || pending)
|
|
18
|
+
return;
|
|
19
|
+
setPending(true);
|
|
20
|
+
// A rejected retry means "still failing": the caller owns that error and
|
|
21
|
+
// keeps showing the banner, so ignoring it here only stops it becoming an
|
|
22
|
+
// unhandled rejection.
|
|
23
|
+
void Promise.resolve(onRetry())
|
|
24
|
+
.catch((_error) => undefined)
|
|
25
|
+
.finally(() => setPending(false));
|
|
26
|
+
};
|
|
27
|
+
return (_jsx("div", { ...rest, children: _jsx(Alert, { status: "error", live: false, children: _jsxs("div", { className: cm.stack(2), children: [_jsx("p", { className: cm.textSize('sm'), children: message }), detail ? (_jsx("p", { className: cm.cn(cm.textSize('xs'), 'text-on-surface-variant'), children: detail })) : null, children, onRetry ? (_jsx("div", { className: cm.flex({ justify: 'start' }), children: _jsx(Button, { variant: "outline", size: "sm", onClick: handleRetry, disabled: pending, "data-mol-id": retryMolId, children: pending
|
|
28
|
+
? t('loadError.retrying', {}, { defaultValue: 'Retrying…' })
|
|
29
|
+
: (retryLabel ?? t('loadError.retry', {}, { defaultValue: 'Retry' })) }) })) : null] }) }) }));
|
|
30
|
+
}
|
|
31
|
+
//# sourceMappingURL=LoadErrorBanner.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"LoadErrorBanner.js","sourceRoot":"","sources":["../../src/components/LoadErrorBanner.tsx"],"names":[],"mappings":";AA0CA,OAAc,EAAE,QAAQ,EAAE,MAAM,OAAO,CAAA;AAEvC,OAAO,EAAE,cAAc,EAAE,MAAM,qBAAqB,CAAA;AACpD,OAAO,EAAE,WAAW,EAAE,MAAM,kBAAkB,CAAA;AAE9C,OAAO,EAAE,KAAK,EAAE,MAAM,YAAY,CAAA;AAClC,OAAO,EAAE,MAAM,EAAE,MAAM,aAAa,CAAA;AA4BpC;;;;GAIG;AACH,MAAM,UAAU,eAAe,CAAC,EAC9B,OAAO,EACP,MAAM,EACN,OAAO,EACP,UAAU,EACV,UAAU,GAAG,kBAAkB,EAC/B,QAAQ,EACR,GAAG,IAAI,EACc;IACrB,MAAM,EAAE,GAAG,WAAW,EAAE,CAAA;IACxB,MAAM,EAAE,CAAC,EAAE,GAAG,cAAc,EAAE,CAAA;IAC9B,MAAM,CAAC,OAAO,EAAE,UAAU,CAAC,GAAG,QAAQ,CAAC,KAAK,CAAC,CAAA;IAE7C,MAAM,WAAW,GAAG,GAAS,EAAE;QAC7B,IAAI,CAAC,OAAO,IAAI,OAAO;YAAE,OAAM;QAC/B,UAAU,CAAC,IAAI,CAAC,CAAA;QAChB,yEAAyE;QACzE,0EAA0E;QAC1E,uBAAuB;QACvB,KAAK,OAAO,CAAC,OAAO,CAAC,OAAO,EAAE,CAAC;aAC5B,KAAK,CAAC,CAAC,MAAe,EAAE,EAAE,CAAC,SAAS,CAAC;aACrC,OAAO,CAAC,GAAG,EAAE,CAAC,UAAU,CAAC,KAAK,CAAC,CAAC,CAAA;IACrC,CAAC,CAAA;IAED,OAAO,CACL,iBAAS,IAAI,YACX,KAAC,KAAK,IAAC,MAAM,EAAC,OAAO,EAAC,IAAI,EAAE,KAAK,YAC/B,eAAK,SAAS,EAAE,EAAE,CAAC,KAAK,CAAC,CAAC,CAAC,aACzB,YAAG,SAAS,EAAE,EAAE,CAAC,QAAQ,CAAC,IAAI,CAAC,YAAG,OAAO,GAAK,EAC7C,MAAM,CAAC,CAAC,CAAC,CACR,YAAG,SAAS,EAAE,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,QAAQ,CAAC,IAAI,CAAC,EAAE,yBAAyB,CAAC,YAAG,MAAM,GAAK,CAChF,CAAC,CAAC,CAAC,IAAI,EACP,QAAQ,EACR,OAAO,CAAC,CAAC,CAAC,CACT,cAAK,SAAS,EAAE,EAAE,CAAC,IAAI,CAAC,EAAE,OAAO,EAAE,OAAO,EAAE,CAAC,YAC3C,KAAC,MAAM,IACL,OAAO,EAAC,SAAS,EACjB,IAAI,EAAC,IAAI,EACT,OAAO,EAAE,WAAW,EACpB,QAAQ,EAAE,OAAO,iBACJ,UAAU,YAEtB,OAAO;gCACN,CAAC,CAAC,CAAC,CAAC,oBAAoB,EAAE,EAAE,EAAE,EAAE,YAAY,EAAE,WAAW,EAAE,CAAC;gCAC5D,CAAC,CAAC,CAAC,UAAU,IAAI,CAAC,CAAC,iBAAiB,EAAE,EAAE,EAAE,EAAE,YAAY,EAAE,OAAO,EAAE,CAAC,CAAC,GAChE,GACL,CACP,CAAC,CAAC,CAAC,IAAI,IACJ,GACA,GACJ,CACP,CAAA;AACH,CAAC"}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PromptDialog component — the shared "ask for a short string" modal.
|
|
3
|
+
*
|
|
4
|
+
* The shared home for what `window.prompt` used to do (renaming, renaming a
|
|
5
|
+
* board, creating a tag, "invite by email"). `window.prompt` is broken
|
|
6
|
+
* headless (it hangs e2e) and awkward on touch; this renders the same
|
|
7
|
+
* interaction as a real dialog built on {@link Modal}. For a confirm WITHOUT
|
|
8
|
+
* an input use {@link ConfirmDialog}; for an INLINE two-click arm/commit
|
|
9
|
+
* button use {@link ConfirmButton}.
|
|
10
|
+
*
|
|
11
|
+
* @example
|
|
12
|
+
* ```tsx
|
|
13
|
+
* import { PromptDialog } from '@molecule/app-ui-react'
|
|
14
|
+
*
|
|
15
|
+
* <PromptDialog
|
|
16
|
+
* open={renameOpen}
|
|
17
|
+
* onClose={() => setRenameOpen(false)}
|
|
18
|
+
* title={t('board.renameTitle', {}, { defaultValue: 'Rename board' })}
|
|
19
|
+
* placeholder={t('board.namePlaceholder', {}, { defaultValue: 'Board name' })}
|
|
20
|
+
* initialValue={board.name}
|
|
21
|
+
* confirmLabel={t('common.save', {}, { defaultValue: 'Save' })}
|
|
22
|
+
* onSubmit={async (name) => {
|
|
23
|
+
* await renameBoard(board.id, name) // resolve → dialog closes
|
|
24
|
+
* }}
|
|
25
|
+
* />
|
|
26
|
+
* ```
|
|
27
|
+
*
|
|
28
|
+
* @remarks
|
|
29
|
+
* `onSubmit` receives the RAW entered value (no trimming — the caller owns
|
|
30
|
+
* validation). Resolve it and the dialog closes and resets; reject or throw
|
|
31
|
+
* and the dialog STAYS OPEN with the value intact — render your error
|
|
32
|
+
* message through `children` (e.g. an `<Alert status="error">`), since the
|
|
33
|
+
* rejection itself is deliberately swallowed here. While the promise is
|
|
34
|
+
* pending the input and both buttons are disabled, so the action cannot
|
|
35
|
+
* double-fire. Enter submits; Escape and the backdrop cancel exactly like a
|
|
36
|
+
* Modal. Optional `inputMolId` / `confirmMolId` / `cancelMolId` render as
|
|
37
|
+
* `data-mol-id` attributes for e2e locators; the buttons are also
|
|
38
|
+
* text-labeled so `getByRole('button', { name })` reaches them.
|
|
39
|
+
*
|
|
40
|
+
* WIZARD GOTCHA (learned the hard way, fleet migration 2026-09-24): a
|
|
41
|
+
* fulfilled submit calls `onClose()` — "close", not "cancel". A multi-stage
|
|
42
|
+
* flow that advances its stage INSIDE `onSubmit` gets clobbered if its
|
|
43
|
+
* `onClose` unconditionally resets to idle (the auto-close fires right
|
|
44
|
+
* after the stage advance). Make `onClose` stage-aware via functional
|
|
45
|
+
* state (`setStage((cur) => (cur === 'name' ? 'idle' : cur))`) so cancel
|
|
46
|
+
* still resets but the post-submit auto-close doesn't undo the advance.
|
|
47
|
+
*
|
|
48
|
+
* @module
|
|
49
|
+
*/
|
|
50
|
+
import React, { type JSX } from 'react';
|
|
51
|
+
/** Props for {@link PromptDialog}. */
|
|
52
|
+
export interface PromptDialogProps {
|
|
53
|
+
/** Whether the dialog is open. */
|
|
54
|
+
open: boolean;
|
|
55
|
+
/** Called when the dialog should close (escape, backdrop, cancel, or a fulfilled {@link PromptDialogProps.onSubmit}). */
|
|
56
|
+
onClose: () => void;
|
|
57
|
+
/** Dialog heading. */
|
|
58
|
+
title: React.ReactNode;
|
|
59
|
+
/** Explanatory line under the title. */
|
|
60
|
+
description?: React.ReactNode;
|
|
61
|
+
/** Input placeholder. */
|
|
62
|
+
placeholder?: string;
|
|
63
|
+
/** Value the input starts from every time the dialog opens. */
|
|
64
|
+
initialValue?: string;
|
|
65
|
+
/**
|
|
66
|
+
* Called with the entered value on confirm. Resolve to close the dialog;
|
|
67
|
+
* reject or throw to keep it open (show the error via {@link PromptDialogProps.children}).
|
|
68
|
+
*/
|
|
69
|
+
onSubmit: (value: string) => void | Promise<void>;
|
|
70
|
+
/** Confirm-button label. Falls back to a localized "Confirm". */
|
|
71
|
+
confirmLabel?: React.ReactNode;
|
|
72
|
+
/** Cancel-button label. Falls back to a localized "Cancel". */
|
|
73
|
+
cancelLabel?: React.ReactNode;
|
|
74
|
+
/** Render the confirm button in the error color (e.g. an irreversible reset). */
|
|
75
|
+
destructive?: boolean;
|
|
76
|
+
/** `data-mol-id` for the text input. */
|
|
77
|
+
inputMolId?: string;
|
|
78
|
+
/** `data-mol-id` for the confirm button. */
|
|
79
|
+
confirmMolId?: string;
|
|
80
|
+
/** `data-mol-id` for the cancel button. */
|
|
81
|
+
cancelMolId?: string;
|
|
82
|
+
/** Extra body content under the input (error banners, hints). */
|
|
83
|
+
children?: React.ReactNode;
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Modal that asks the user for a short string, replacing `window.prompt`.
|
|
87
|
+
*
|
|
88
|
+
* @param props - Component props (see {@link PromptDialogProps}).
|
|
89
|
+
*/
|
|
90
|
+
export declare function PromptDialog({ open, onClose, title, description, placeholder, initialValue, onSubmit, confirmLabel, cancelLabel, destructive, inputMolId, confirmMolId, cancelMolId, children, }: PromptDialogProps): JSX.Element;
|
|
91
|
+
//# sourceMappingURL=PromptDialog.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"PromptDialog.d.ts","sourceRoot":"","sources":["../../src/components/PromptDialog.tsx"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAgDG;AAEH,OAAO,KAAK,EAAE,EAAE,KAAK,GAAG,EAAuB,MAAM,OAAO,CAAA;AAS5D,sCAAsC;AACtC,MAAM,WAAW,iBAAiB;IAChC,kCAAkC;IAClC,IAAI,EAAE,OAAO,CAAA;IACb,yHAAyH;IACzH,OAAO,EAAE,MAAM,IAAI,CAAA;IACnB,sBAAsB;IACtB,KAAK,EAAE,KAAK,CAAC,SAAS,CAAA;IACtB,wCAAwC;IACxC,WAAW,CAAC,EAAE,KAAK,CAAC,SAAS,CAAA;IAC7B,yBAAyB;IACzB,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,+DAA+D;IAC/D,YAAY,CAAC,EAAE,MAAM,CAAA;IACrB;;;OAGG;IACH,QAAQ,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;IACjD,iEAAiE;IACjE,YAAY,CAAC,EAAE,KAAK,CAAC,SAAS,CAAA;IAC9B,+DAA+D;IAC/D,WAAW,CAAC,EAAE,KAAK,CAAC,SAAS,CAAA;IAC7B,iFAAiF;IACjF,WAAW,CAAC,EAAE,OAAO,CAAA;IACrB,wCAAwC;IACxC,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB,4CAA4C;IAC5C,YAAY,CAAC,EAAE,MAAM,CAAA;IACrB,2CAA2C;IAC3C,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,iEAAiE;IACjE,QAAQ,CAAC,EAAE,KAAK,CAAC,SAAS,CAAA;CAC3B;AAED;;;;GAIG;AACH,wBAAgB,YAAY,CAAC,EAC3B,IAAI,EACJ,OAAO,EACP,KAAK,EACL,WAAW,EACX,WAAW,EACX,YAAiB,EACjB,QAAQ,EACR,YAAY,EACZ,WAAW,EACX,WAAmB,EACnB,UAAU,EACV,YAAY,EACZ,WAAW,EACX,QAAQ,GACT,EAAE,iBAAiB,GAAG,GAAG,CAAC,OAAO,CAoEjC"}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
/**
|
|
3
|
+
* PromptDialog component — the shared "ask for a short string" modal.
|
|
4
|
+
*
|
|
5
|
+
* The shared home for what `window.prompt` used to do (renaming, renaming a
|
|
6
|
+
* board, creating a tag, "invite by email"). `window.prompt` is broken
|
|
7
|
+
* headless (it hangs e2e) and awkward on touch; this renders the same
|
|
8
|
+
* interaction as a real dialog built on {@link Modal}. For a confirm WITHOUT
|
|
9
|
+
* an input use {@link ConfirmDialog}; for an INLINE two-click arm/commit
|
|
10
|
+
* button use {@link ConfirmButton}.
|
|
11
|
+
*
|
|
12
|
+
* @example
|
|
13
|
+
* ```tsx
|
|
14
|
+
* import { PromptDialog } from '@molecule/app-ui-react'
|
|
15
|
+
*
|
|
16
|
+
* <PromptDialog
|
|
17
|
+
* open={renameOpen}
|
|
18
|
+
* onClose={() => setRenameOpen(false)}
|
|
19
|
+
* title={t('board.renameTitle', {}, { defaultValue: 'Rename board' })}
|
|
20
|
+
* placeholder={t('board.namePlaceholder', {}, { defaultValue: 'Board name' })}
|
|
21
|
+
* initialValue={board.name}
|
|
22
|
+
* confirmLabel={t('common.save', {}, { defaultValue: 'Save' })}
|
|
23
|
+
* onSubmit={async (name) => {
|
|
24
|
+
* await renameBoard(board.id, name) // resolve → dialog closes
|
|
25
|
+
* }}
|
|
26
|
+
* />
|
|
27
|
+
* ```
|
|
28
|
+
*
|
|
29
|
+
* @remarks
|
|
30
|
+
* `onSubmit` receives the RAW entered value (no trimming — the caller owns
|
|
31
|
+
* validation). Resolve it and the dialog closes and resets; reject or throw
|
|
32
|
+
* and the dialog STAYS OPEN with the value intact — render your error
|
|
33
|
+
* message through `children` (e.g. an `<Alert status="error">`), since the
|
|
34
|
+
* rejection itself is deliberately swallowed here. While the promise is
|
|
35
|
+
* pending the input and both buttons are disabled, so the action cannot
|
|
36
|
+
* double-fire. Enter submits; Escape and the backdrop cancel exactly like a
|
|
37
|
+
* Modal. Optional `inputMolId` / `confirmMolId` / `cancelMolId` render as
|
|
38
|
+
* `data-mol-id` attributes for e2e locators; the buttons are also
|
|
39
|
+
* text-labeled so `getByRole('button', { name })` reaches them.
|
|
40
|
+
*
|
|
41
|
+
* WIZARD GOTCHA (learned the hard way, fleet migration 2026-09-24): a
|
|
42
|
+
* fulfilled submit calls `onClose()` — "close", not "cancel". A multi-stage
|
|
43
|
+
* flow that advances its stage INSIDE `onSubmit` gets clobbered if its
|
|
44
|
+
* `onClose` unconditionally resets to idle (the auto-close fires right
|
|
45
|
+
* after the stage advance). Make `onClose` stage-aware via functional
|
|
46
|
+
* state (`setStage((cur) => (cur === 'name' ? 'idle' : cur))`) so cancel
|
|
47
|
+
* still resets but the post-submit auto-close doesn't undo the advance.
|
|
48
|
+
*
|
|
49
|
+
* @module
|
|
50
|
+
*/
|
|
51
|
+
import { useEffect, useState } from 'react';
|
|
52
|
+
import { useTranslation } from '@molecule/app-react';
|
|
53
|
+
import { getClassMap } from '@molecule/app-ui';
|
|
54
|
+
import { Button } from './Button.js';
|
|
55
|
+
import { Input } from './Input.js';
|
|
56
|
+
import { Modal } from './Modal.js';
|
|
57
|
+
/**
|
|
58
|
+
* Modal that asks the user for a short string, replacing `window.prompt`.
|
|
59
|
+
*
|
|
60
|
+
* @param props - Component props (see {@link PromptDialogProps}).
|
|
61
|
+
*/
|
|
62
|
+
export function PromptDialog({ open, onClose, title, description, placeholder, initialValue = '', onSubmit, confirmLabel, cancelLabel, destructive = false, inputMolId, confirmMolId, cancelMolId, children, }) {
|
|
63
|
+
const cm = getClassMap();
|
|
64
|
+
const { t } = useTranslation();
|
|
65
|
+
const [value, setValue] = useState(initialValue);
|
|
66
|
+
const [pending, setPending] = useState(false);
|
|
67
|
+
// Re-seed from initialValue each open so a reopened dialog shows the
|
|
68
|
+
// CURRENT value, not whatever the user typed last time.
|
|
69
|
+
useEffect(() => {
|
|
70
|
+
if (open) {
|
|
71
|
+
setValue(initialValue);
|
|
72
|
+
setPending(false);
|
|
73
|
+
}
|
|
74
|
+
}, [open, initialValue]);
|
|
75
|
+
const handleSubmit = (e) => {
|
|
76
|
+
e.preventDefault();
|
|
77
|
+
if (pending)
|
|
78
|
+
return;
|
|
79
|
+
setPending(true);
|
|
80
|
+
void Promise.resolve(onSubmit(value))
|
|
81
|
+
.then(() => onClose())
|
|
82
|
+
.catch(() => {
|
|
83
|
+
// Deliberate: a rejection keeps the dialog open (see @remarks);
|
|
84
|
+
// the caller renders the error through `children`.
|
|
85
|
+
setPending(false);
|
|
86
|
+
});
|
|
87
|
+
};
|
|
88
|
+
return (_jsx(Modal, { open: open, onClose: pending ? () => { } : onClose, children: _jsx("form", { onSubmit: handleSubmit, children: _jsxs("div", { className: cm.stack(3), children: [_jsx("h2", { className: cm.cn(cm.textSize('lg'), cm.fontWeight('bold')), children: title }), description ? _jsx("p", { className: cm.textSize('sm'), children: description }) : null, _jsx(Input, { value: value, onChange: (e) => setValue(e.target.value), placeholder: placeholder, "aria-label": typeof title === 'string' ? title : placeholder, disabled: pending, autoFocus: true, "data-mol-id": inputMolId }), children, _jsxs("div", { className: cm.flex({ justify: 'end', gap: 'sm' }), children: [_jsx(Button, { type: "button", variant: "ghost", onClick: onClose, disabled: pending, "data-mol-id": cancelMolId, children: cancelLabel ?? t('prompt.cancel', {}, { defaultValue: 'Cancel' }) }), _jsx(Button, { type: "submit", variant: "solid", color: destructive ? 'error' : 'primary', disabled: pending, "data-mol-id": confirmMolId, children: confirmLabel ?? t('prompt.confirm', {}, { defaultValue: 'Confirm' }) })] })] }) }) }));
|
|
89
|
+
}
|
|
90
|
+
//# sourceMappingURL=PromptDialog.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"PromptDialog.js","sourceRoot":"","sources":["../../src/components/PromptDialog.tsx"],"names":[],"mappings":"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"}
|