@enonic/ui-kit 0.3.0 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (50) hide show
  1. package/README.md +78 -8
  2. package/dist/dialog/action-dialog-context.d.ts +22 -0
  3. package/dist/dialog/action-dialog-context.js +21 -0
  4. package/dist/dialog/action-dialog-context.js.map +1 -0
  5. package/dist/dialog/action-dialog.d.ts +135 -0
  6. package/dist/dialog/action-dialog.js +213 -0
  7. package/dist/dialog/action-dialog.js.map +1 -0
  8. package/dist/dialog/delete-confirm.d.ts +33 -0
  9. package/dist/dialog/delete-confirm.js +80 -0
  10. package/dist/dialog/delete-confirm.js.map +1 -0
  11. package/dist/dialog/delete-expectation.d.ts +17 -0
  12. package/dist/dialog/delete-expectation.js +13 -0
  13. package/dist/dialog/delete-expectation.js.map +1 -0
  14. package/dist/dialog/dialog-preset.d.ts +34 -0
  15. package/dist/dialog/dialog-preset.js +38 -0
  16. package/dist/dialog/dialog-preset.js.map +1 -0
  17. package/dist/dialog/gate-match.d.ts +6 -0
  18. package/dist/dialog/gate-match.js +11 -0
  19. package/dist/dialog/gate-match.js.map +1 -0
  20. package/dist/dialog/gate.d.ts +31 -0
  21. package/dist/dialog/gate.js +72 -0
  22. package/dist/dialog/gate.js.map +1 -0
  23. package/dist/dialog/use-close-guard.d.ts +34 -0
  24. package/dist/dialog/use-close-guard.js +40 -0
  25. package/dist/dialog/use-close-guard.js.map +1 -0
  26. package/dist/filter/filter-fields.d.ts +33 -0
  27. package/dist/filter/filter-fields.js +72 -0
  28. package/dist/filter/filter-fields.js.map +1 -0
  29. package/dist/filter/filter-input.d.ts +35 -0
  30. package/dist/filter/filter-input.js +441 -0
  31. package/dist/filter/filter-input.js.map +1 -0
  32. package/dist/filter/filter-input.phrases.d.ts +26 -0
  33. package/dist/filter/filter-input.phrases.js +26 -0
  34. package/dist/filter/filter-input.phrases.js.map +1 -0
  35. package/dist/filter/filter-query.d.ts +40 -0
  36. package/dist/filter/filter-query.js +61 -0
  37. package/dist/filter/filter-query.js.map +1 -0
  38. package/dist/i18n/fill-phrase.d.ts +10 -0
  39. package/dist/i18n/fill-phrase.js +25 -0
  40. package/dist/i18n/fill-phrase.js.map +1 -0
  41. package/dist/i18n/phrases.d.ts +14 -0
  42. package/dist/i18n/phrases.js +23 -0
  43. package/dist/i18n/phrases.js.map +1 -0
  44. package/dist/i18n/use-phrases.d.ts +9 -0
  45. package/dist/i18n/use-phrases.js +11 -0
  46. package/dist/i18n/use-phrases.js.map +1 -0
  47. package/dist/index.d.ts +16 -1
  48. package/dist/index.js +15 -0
  49. package/package.json +34 -6
  50. package/preset.css +6 -0
package/README.md CHANGED
@@ -1,24 +1,21 @@
1
1
  # @enonic/ui-kit
2
2
 
3
3
  Composite React components for Enonic applications — what
4
- [`@enonic/ui`](https://www.npmjs.com/package/@enonic/ui) would be if its parts carried behaviour:
5
- layouts, split panels, toolbars, browse screens with their list and details columns, dialog and
6
- form shells.
4
+ [`@enonic/ui`](https://www.npmjs.com/package/@enonic/ui) would be if its parts carried behaviour.
5
+ The dialog with an outcome and its confirmations are here, and the filter input of a browse screen;
6
+ layouts, toolbars and the rest of the browse screen follow.
7
7
 
8
8
  A component belongs here when it holds state, coordinates several base components, or encodes a
9
9
  screen pattern more than one application repeats. Props in, callbacks out: nothing here reaches
10
10
  into a host application's configuration, stores, router or phrase keys.
11
11
 
12
- > **Status**: scaffolding, pre-1.0. The build and release path work end to end; the components
13
- > land package by package.
14
-
15
12
  ## Install
16
13
 
17
14
  Pick one framework — the same contract as `@enonic/ui`:
18
15
 
19
16
  ```sh
20
- pnpm add @enonic/ui-kit react react-dom # React
21
- pnpm add @enonic/ui-kit preact # Preact
17
+ pnpm add @enonic/ui-kit @enonic/ui react react-dom # React
18
+ pnpm add @enonic/ui-kit @enonic/ui preact # Preact
22
19
  ```
23
20
 
24
21
  The components are written as React code. On Preact, map React's names to `preact/compat` in the
@@ -35,4 +32,77 @@ resolve: {
35
32
  },
36
33
  ```
37
34
 
35
+ ## Styles
36
+
37
+ The components carry Tailwind classes and ship no CSS: the application's Tailwind build generates
38
+ what they use. Import the kit's preset after `@enonic/ui`'s — it points the build at the package's
39
+ `dist`, and the tokens the classes use are `@enonic/ui`'s:
40
+
41
+ ```css
42
+ @import 'tailwindcss';
43
+ @import '@enonic/ui/preset.css';
44
+ @import '@enonic/ui-kit/preset.css';
45
+ ```
46
+
47
+ ## Labels
48
+
49
+ Every text a component renders on its own — a Cancel, a Confirm, the gate's hint — is English in
50
+ `uiKitPhrases`, keyed `enonic.uiKit.<area>.<name>`, and goes through the `Translate` the
51
+ application hands `@enonic/ui`'s `I18nProvider`; a key the application has no text for renders the
52
+ English. Titles, questions and target labels are props. `comparePhrases` from `@enonic/ui-utils`
53
+ tells an application which keys its bundle lacks.
54
+
55
+ ## What is here
56
+
57
+ | Export | What it does |
58
+ | ---------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
59
+ | `ActionDialog` | `Dialog` from `@enonic/ui` plus an outcome: a `Content` that holds whether the action may be taken, a `Body` that goes inert while the footer asks, a `Footer` that takes the action or asks |
60
+ | `ActionDialog.Footer` | Cancel and Confirm when given `onConfirm`, or the controls it is given as children — a wizard's step indicator; `intent`, `error`, `confirmRef`; `question` asks in place of the controls |
61
+ | `ActionDialog.Action` | a further button of the footer, enabled by the content like Confirm; `intent`, `closeOnClick` |
62
+ | `useCloseGuard({ dirty, open, onOpenChange })` | the dirty-close question: the mask, the X, `Escape` and Cancel ask before a close that would lose something; `asking` feeds the footer's `question`, `keep` returns the focus |
63
+ | `Gate` | `Root`, `Hint` naming what to type back, `Input` that enables the confirm button on a match and locks |
64
+ | `DialogPreset.Confirm` | the plain question: title, question, two answers of equal weight |
65
+ | `DeleteConfirm` | the delete view: question, targets, the gate asking for the one name or the count, a red button — the first view, or the view after a list, with `closeOnCancel` off to go back to it |
66
+ | `deleteExpectation(targets)` | the one name, or the count |
67
+ | `matchesExpected(typed, expected)` | the gate's comparison, spaces around the entry forgiven |
68
+ | `fillPhrase(phrase, values)` | a phrase with its `{n}` placeholders replaced by nodes |
69
+ | `uiKitPhrases`, `useUiKitPhrases()` | the catalogue, and the hook that resolves it through the application's `Translate` |
70
+ | `useActionDialog()` | the content's state, for anything inside it that enables the action |
71
+ | `FilterInput` | one input for a list's filter: values of predefined `fields` combined with free text, every term a `Tag`; `value`/`onChange` or `defaultValue`; `disabled`, `readOnly` (the tags alone, no input), `error`; the ref is the text input's |
72
+ | `FilterField`, `FilterValue` | what the dropdown offers: a field with an id, a label, an icon, its values, `multiple` and a `notice`; a value with a `count`, a `group` and `disabled` |
73
+ | `FilterQuery`, `FilterTerm` | what the list is narrowed by: `FieldTerm` and `TextTerm`, discriminated by `kind`, ids never labels, in the order they were added; `fieldTerm`, `textTerm`, `termKey` |
74
+ | `textOf(query)`, `valuesOf(query, field)` | the two readings a list takes of a query: the free text as one string, and every value picked for a field — empty narrows nothing |
75
+ | `withTerm`, `withoutTerm`, `toggledTerm` | the pure operations on a query the input is built from, with `sameTerm`, `isTextTerm`, `isFieldTerm` and `EMPTY_FILTER` |
76
+ | `filterInputPhrases` | the input's English, for an application's `comparePhrases` |
77
+
78
+ ## The filter
79
+
80
+ `FilterInput` is the GitHub-style filter of a browse screen. Its dropdown offers the `fields` the
81
+ caller passes — each with an id, a label, an icon and the values it can take; a value carries a
82
+ `count` where the list knows its hits, a `group` where values are listed under a labelled heading,
83
+ and `disabled` where it is withheld for another reason; a field with `multiple: false` holds one
84
+ value at a time, one with a `notice` says above its values why they may be short or stale, and one
85
+ with `loading` says its values are still on their way. The
86
+ dropdown opens on a click, a keystroke or an arrow, not on focus. The fields come first; picking
87
+ one, or typing its label and a colon — or pasting `Field: value` whole — opens its values ordered by
88
+ hits, the empty ones last and disabled, with a `No matches` row when the typed text finds none.
89
+ Enter picks the highlighted value, and the values stay open so several can be ticked; a tick picked
90
+ again, or the tag's cross, takes the term back. Typed text with no field entered becomes a tag on
91
+ Enter. Backspace in the empty input steps out of a field, or takes the last tag back; ArrowLeft at
92
+ the start of the input walks onto the tags, where arrows move between them and Backspace or Delete
93
+ removes one. The right-hand cross clears everything, and leaving the filter drops a field no value
94
+ was picked for. Every add, remove and clearing is announced to assistive technology.
95
+
96
+ The query is a list of terms, and the component decides nothing about what they mean. The reading
97
+ the helpers encode, and the one the list should match by, is: values of one field are ORed, fields
98
+ are ANDed, and free text is ANDed with everything, word by word, without regard to case.
99
+
100
+ ```tsx
101
+ const [query, setQuery] = useState<FilterQuery>(EMPTY_FILTER);
102
+ const providers = valuesOf(query, 'idProvider'); // a Set of ids; empty narrows nothing
103
+ const text = textOf(query); // 'alice ward'
104
+
105
+ <FilterInput fields={fields} value={query} onChange={setQuery} placeholder="Search users" />;
106
+ ```
107
+
38
108
  Part of the [Enonic UI Toolkit](https://github.com/enonic/npm-enonic-ui-toolkit).
@@ -0,0 +1,22 @@
1
+ import { MutableRefObject, ReactElement, ReactNode } from "react";
2
+ //#region src/dialog/action-dialog-context.d.ts
3
+ type ActionDialogContextValue = {
4
+ /** Whether the action may be taken: anything inside the content may decide, a gate does. */
5
+ confirmEnabled: boolean;
6
+ setConfirmEnabled: (next: boolean) => void;
7
+ /** Whether the footer is asking its question, during which the body is inert. */
8
+ asking: boolean;
9
+ setAsking: (next: boolean) => void;
10
+ /** The way back from the question, for `Escape`. */
11
+ keepRef: MutableRefObject<(() => void) | undefined>;
12
+ /** The element inside the content that last had the focus, for the way back from the question. */
13
+ lastFocusRef: MutableRefObject<HTMLElement | null>;
14
+ };
15
+ declare const useActionDialog: () => ActionDialogContextValue;
16
+ /** Why `onOpenChange` closes: `action` when an `Action` or the footer's Confirm took the outcome. */
17
+ type ActionDialogOpenChangeDetails = {
18
+ reason: 'action';
19
+ };
20
+ //#endregion
21
+ export { ActionDialogContextValue, ActionDialogOpenChangeDetails, useActionDialog };
22
+ //# sourceMappingURL=action-dialog-context.d.ts.map
@@ -0,0 +1,21 @@
1
+ import { createContext, useContext } from "react";
2
+ import { jsx } from "react/jsx-runtime";
3
+ //#region src/dialog/action-dialog-context.tsx
4
+ const ActionDialogContext = createContext(void 0);
5
+ const ActionDialogProvider = ({ value, children }) => /* @__PURE__ */ jsx(ActionDialogContext.Provider, {
6
+ value,
7
+ children
8
+ });
9
+ ActionDialogProvider.displayName = "ActionDialogProvider";
10
+ const useActionDialog = () => {
11
+ const context = useContext(ActionDialogContext);
12
+ if (context === void 0) throw new Error("useActionDialog must be used within ActionDialog.Content");
13
+ return context;
14
+ };
15
+ const ActionDialogRootContext = createContext(void 0);
16
+ const ActionDialogRootProvider = ActionDialogRootContext.Provider;
17
+ const useActionDialogRoot = () => useContext(ActionDialogRootContext);
18
+ //#endregion
19
+ export { ActionDialogProvider, ActionDialogRootProvider, useActionDialog, useActionDialogRoot };
20
+
21
+ //# sourceMappingURL=action-dialog-context.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"action-dialog-context.js","names":[],"sources":["../../src/dialog/action-dialog-context.tsx"],"sourcesContent":["import {\n createContext,\n type MutableRefObject,\n type ReactElement,\n type ReactNode,\n useContext,\n} from 'react';\n\nexport type ActionDialogContextValue = {\n /** Whether the action may be taken: anything inside the content may decide, a gate does. */\n confirmEnabled: boolean;\n setConfirmEnabled: (next: boolean) => void;\n /** Whether the footer is asking its question, during which the body is inert. */\n asking: boolean;\n setAsking: (next: boolean) => void;\n /** The way back from the question, for `Escape`. */\n keepRef: MutableRefObject<(() => void) | undefined>;\n /** The element inside the content that last had the focus, for the way back from the question. */\n lastFocusRef: MutableRefObject<HTMLElement | null>;\n};\n\nconst ActionDialogContext = createContext<ActionDialogContextValue | undefined>(undefined);\n\nexport type ActionDialogProviderProps = {\n value: ActionDialogContextValue;\n children?: ReactNode;\n};\n\nexport const ActionDialogProvider = ({\n value,\n children,\n}: ActionDialogProviderProps): ReactElement => (\n <ActionDialogContext.Provider value={value}>{children}</ActionDialogContext.Provider>\n);\nActionDialogProvider.displayName = 'ActionDialogProvider';\n\nexport const useActionDialog = (): ActionDialogContextValue => {\n const context = useContext(ActionDialogContext);\n if (context === undefined) {\n throw new Error('useActionDialog must be used within ActionDialog.Content');\n }\n return context;\n};\n\n/** Why `onOpenChange` closes: `action` when an `Action` or the footer's Confirm took the outcome. */\nexport type ActionDialogOpenChangeDetails = { reason: 'action' };\n\nexport type ActionDialogRootContextValue = {\n /** Set by an action that closes, read by the close it causes. */\n actionCloseRef: MutableRefObject<boolean>;\n};\n\nconst ActionDialogRootContext = createContext<ActionDialogRootContextValue | undefined>(undefined);\n\nexport const ActionDialogRootProvider = ActionDialogRootContext.Provider;\n\nexport const useActionDialogRoot = (): ActionDialogRootContextValue | undefined =>\n useContext(ActionDialogRootContext);\n"],"mappings":";;;AAqBA,MAAM,sBAAsB,cAAoD,KAAA,CAAS;AAOzF,MAAa,wBAAwB,EACnC,OACA,eAEA,oBAAC,oBAAoB,UAArB;CAAqC;CAAQ;AAAuC,CAAA;AAEtF,qBAAqB,cAAc;AAEnC,MAAa,wBAAkD;CAC7D,MAAM,UAAU,WAAW,mBAAmB;CAC9C,IAAI,YAAY,KAAA,GACd,MAAM,IAAI,MAAM,0DAA0D;CAE5E,OAAO;AACT;AAUA,MAAM,0BAA0B,cAAwD,KAAA,CAAS;AAEjG,MAAa,2BAA2B,wBAAwB;AAEhE,MAAa,4BACX,WAAW,uBAAuB"}
@@ -0,0 +1,135 @@
1
+ import { ActionDialogOpenChangeDetails } from "./action-dialog-context.js";
2
+ import { ComponentPropsWithoutRef, ReactElement, ReactNode, Ref } from "react";
3
+ import { Button, ButtonVariant, Dialog, DialogRootProps } from "@enonic/ui";
4
+ //#region src/dialog/action-dialog.d.ts
5
+ type DialogIntent = 'default' | 'danger';
6
+ /** A question needs no more room than its own text; `medium` carries a control, `wide` a form. */
7
+ type DialogSize = 'default' | 'medium' | 'wide';
8
+ type ActionDialogRootProps = Omit<DialogRootProps, 'onOpenChange'> & {
9
+ /** `details` tells a close an action took from one that would leave without it. */
10
+ onOpenChange?: (open: boolean, details?: ActionDialogOpenChangeDetails) => void;
11
+ };
12
+ declare const ActionDialogRoot: {
13
+ ({ onOpenChange, ...props }: ActionDialogRootProps): ReactElement;
14
+ displayName: string;
15
+ };
16
+ type ActionDialogContentProps = {
17
+ size?: DialogSize;
18
+ /** Whether the action starts enabled; a content with a gate starts it disabled. */
19
+ defaultConfirmEnabled?: boolean;
20
+ } & ComponentPropsWithoutRef<typeof Dialog.Content>;
21
+ type ActionDialogBodyProps = ComponentPropsWithoutRef<typeof Dialog.Body>;
22
+ type ActionDialogActionProps = {
23
+ label: string;
24
+ /** `danger` paints the button red, for an action there is no undoing. */
25
+ intent?: DialogIntent;
26
+ /** Enabled while the content allows the action — a gate matched, a form valid — unless disabled outright. */
27
+ disabled?: boolean;
28
+ /** Close the dialog on click; off for a caller that moves on to a progress view instead. */
29
+ closeOnClick?: boolean;
30
+ variant?: ButtonVariant;
31
+ } & Omit<ComponentPropsWithoutRef<typeof Button>, 'label' | 'variant' | 'disabled'>;
32
+ /** A question the footer asks in place of its controls. */
33
+ type FooterQuestion = {
34
+ text: ReactNode;
35
+ intent?: DialogIntent;
36
+ /** Defaults to the kit's "Confirm". */
37
+ confirmLabel?: string;
38
+ /** The way back; defaults to the kit's "Keep editing". */
39
+ keepLabel?: string;
40
+ onConfirm: () => void;
41
+ onKeep: () => void;
42
+ };
43
+ type ActionDialogFooterProps = {
44
+ /** Given `onConfirm`, the footer renders Cancel and Confirm; without it, its children are the controls. */
45
+ onConfirm?: () => void;
46
+ onCancel?: () => void;
47
+ /** Defaults to the kit's "Confirm". */
48
+ confirmLabel?: string;
49
+ /** Defaults to the kit's "Cancel". */
50
+ cancelLabel?: string;
51
+ /** `danger` paints the confirm button red, for an action there is no undoing. */
52
+ intent?: DialogIntent;
53
+ /** Why the dialog is still open, shown beside its controls — a rejected save is the case. */
54
+ error?: string;
55
+ /** For a caller that moves the focus onto the confirm button once it means something. */
56
+ confirmRef?: Ref<HTMLButtonElement>;
57
+ /** `outline` gives the two answers of a question equal weight; a form's Cancel stays quiet. */
58
+ cancelVariant?: ButtonVariant;
59
+ /** Close the dialog on Cancel; off for a caller that closes it itself. */
60
+ closeOnCancel?: boolean;
61
+ /** Close the dialog on Confirm; off for a caller that moves on to a progress view instead. */
62
+ closeOnConfirm?: boolean;
63
+ /**
64
+ * While set, the footer asks this instead of showing its controls: the body goes inert, `Escape`
65
+ * is `onKeep`, and the focus goes to the way back. `useCloseGuard` is the usual source.
66
+ */
67
+ question?: FooterQuestion;
68
+ } & ComponentPropsWithoutRef<typeof Dialog.Footer>;
69
+ /**
70
+ * `Dialog` from `@enonic/ui` plus an outcome: a `Content` that holds whether the action may be
71
+ * taken, a `Body` that goes inert while the footer asks a question, a `Footer` that takes the
72
+ * action or asks the question, and `Action` for a further button the content enables. Stacks
73
+ * nothing: a question goes to the footer, content with room of its own is another `Content` under
74
+ * the same `Root`.
75
+ */
76
+ declare const ActionDialog: {
77
+ displayName: string;
78
+ Trigger: import('preact').FunctionalComponent<import('preact/compat').PropsWithoutRef<import("@enonic/ui").DialogTriggerProps> & {
79
+ ref?: import('preact').Ref<HTMLButtonElement> | undefined;
80
+ }>;
81
+ Portal: {
82
+ ({ children, container, forceMount }: import("@enonic/ui").DialogPortalProps): ReactElement | null;
83
+ displayName: string;
84
+ };
85
+ Overlay: import('preact').FunctionalComponent<import('preact/compat').PropsWithoutRef<import("@enonic/ui").DialogOverlayProps> & {
86
+ ref?: import('preact').Ref<HTMLDivElement> | undefined;
87
+ }>;
88
+ Header: import('preact').FunctionalComponent<import('preact/compat').PropsWithoutRef<import("@enonic/ui").DialogHeaderProps> & {
89
+ ref?: import('preact').Ref<HTMLElement> | undefined;
90
+ }>;
91
+ Close: import('preact').FunctionalComponent<import('preact/compat').PropsWithoutRef<import("@enonic/ui").DialogCloseProps> & {
92
+ ref?: import('preact').Ref<HTMLButtonElement> | undefined;
93
+ }>;
94
+ Title: import('preact').FunctionalComponent<import('preact/compat').PropsWithoutRef<import("@enonic/ui").DialogTitleProps> & {
95
+ ref?: import('preact').Ref<HTMLHeadingElement> | undefined;
96
+ }>;
97
+ Description: import('preact').FunctionalComponent<import('preact/compat').PropsWithoutRef<import("@enonic/ui").DialogDescriptionProps> & {
98
+ ref?: import('preact').Ref<HTMLParagraphElement> | undefined;
99
+ }>;
100
+ DefaultClose: import('preact').FunctionalComponent<import('preact/compat').PropsWithoutRef<Partial<ComponentPropsWithoutRef<import('preact').FunctionalComponent<import('preact/compat').PropsWithoutRef<import("@enonic/ui").IconButtonProps> & {
101
+ ref?: import('preact').Ref<HTMLButtonElement> | undefined;
102
+ }>>>> & {
103
+ ref?: import('preact').Ref<HTMLButtonElement> | undefined;
104
+ }>;
105
+ DefaultHeader: import('preact').FunctionalComponent<import('preact/compat').PropsWithoutRef<import("@enonic/ui").DialogDefaultHeaderProps> & {
106
+ ref?: import('preact').Ref<HTMLElement> | undefined;
107
+ }>;
108
+ StepHeader: import('preact').FunctionalComponent<import('preact/compat').PropsWithoutRef<import("@enonic/ui").DialogStepHeaderProps> & {
109
+ ref?: import('preact').Ref<HTMLElement> | undefined;
110
+ }>;
111
+ StepContent: {
112
+ ({ step, locked, children }: import("@enonic/ui").DialogStepContentProps): ReactElement;
113
+ displayName: string;
114
+ };
115
+ StepIndicator: {
116
+ ({ previousLabel, nextLabel, lastStepLabel, onLastStep, dots, disabled, pending, renderDot }: import("@enonic/ui").DialogStepIndicatorProps): ReactElement;
117
+ displayName: string;
118
+ };
119
+ Root: typeof ActionDialogRoot;
120
+ Content: import("preact").FunctionalComponent<import("preact/compat").PropsWithoutRef<ActionDialogContentProps> & {
121
+ ref?: Ref<HTMLDivElement> | undefined;
122
+ }>;
123
+ Body: import("preact").FunctionalComponent<import("preact/compat").PropsWithoutRef<ActionDialogBodyProps> & {
124
+ ref?: Ref<HTMLDivElement> | undefined;
125
+ }>;
126
+ Footer: import("preact").FunctionalComponent<import("preact/compat").PropsWithoutRef<ActionDialogFooterProps> & {
127
+ ref?: Ref<HTMLElement> | undefined;
128
+ }>;
129
+ Action: import("preact").FunctionalComponent<import("preact/compat").PropsWithoutRef<ActionDialogActionProps> & {
130
+ ref?: Ref<HTMLButtonElement> | undefined;
131
+ }>;
132
+ };
133
+ //#endregion
134
+ export { ActionDialog, ActionDialogActionProps, ActionDialogBodyProps, ActionDialogContentProps, ActionDialogFooterProps, ActionDialogRootProps, DialogIntent, DialogSize, FooterQuestion };
135
+ //# sourceMappingURL=action-dialog.d.ts.map
@@ -0,0 +1,213 @@
1
+ import { ActionDialogProvider, ActionDialogRootProvider, useActionDialog, useActionDialogRoot } from "./action-dialog-context.js";
2
+ import { useUiKitPhrases } from "../i18n/use-phrases.js";
3
+ import { forwardRef, useEffect, useLayoutEffect, useMemo, useRef, useState } from "react";
4
+ import { jsx, jsxs } from "react/jsx-runtime";
5
+ import { Button, Dialog, cn, usePrefixedId } from "@enonic/ui";
6
+ import { TriangleAlert } from "lucide-react";
7
+ //#region src/dialog/action-dialog.tsx
8
+ const SIZES = {
9
+ default: "max-w-lg",
10
+ medium: "max-w-180 sm:min-w-152",
11
+ wide: "max-w-4xl"
12
+ };
13
+ const DANGER_BUTTON_CLASS_NAME = "bg-btn-error text-alt hover:bg-btn-error-hover active:bg-btn-error-active focus-visible:ring-error/50";
14
+ const ActionDialogRoot = ({ onOpenChange, ...props }) => {
15
+ const actionCloseRef = useRef(false);
16
+ const value = useMemo(() => ({ actionCloseRef }), []);
17
+ return /* @__PURE__ */ jsx(ActionDialogRootProvider, {
18
+ value,
19
+ children: /* @__PURE__ */ jsx(Dialog.Root, {
20
+ ...props,
21
+ onOpenChange: (open) => {
22
+ const byAction = actionCloseRef.current;
23
+ actionCloseRef.current = false;
24
+ onOpenChange?.(open, !open && byAction ? { reason: "action" } : void 0);
25
+ }
26
+ })
27
+ });
28
+ };
29
+ ActionDialogRoot.displayName = "ActionDialog.Root";
30
+ const CONTENT_NAME = "ActionDialog.Content";
31
+ const QUESTION_NAME = "ActionDialog.Question";
32
+ const ActionDialogContent = forwardRef(({ size = "default", defaultConfirmEnabled = true, className, children, onEscapeKeyDown, onFocusCapture, ...props }, ref) => {
33
+ const [confirmEnabled, setConfirmEnabled] = useState(defaultConfirmEnabled);
34
+ const [asking, setAsking] = useState(false);
35
+ const keepRef = useRef(void 0);
36
+ const lastFocusRef = useRef(null);
37
+ const wasAskingRef = useRef(false);
38
+ const context = useMemo(() => ({
39
+ confirmEnabled,
40
+ setConfirmEnabled,
41
+ asking,
42
+ setAsking,
43
+ keepRef,
44
+ lastFocusRef
45
+ }), [confirmEnabled, asking]);
46
+ useLayoutEffect(() => {
47
+ const target = lastFocusRef.current;
48
+ const answered = wasAskingRef.current && !asking;
49
+ wasAskingRef.current = asking;
50
+ if (!answered || target === null) return;
51
+ const frame = requestAnimationFrame(() => target.focus());
52
+ return () => cancelAnimationFrame(frame);
53
+ }, [asking]);
54
+ return /* @__PURE__ */ jsx(Dialog.Content, {
55
+ ref,
56
+ "data-component": CONTENT_NAME,
57
+ className: cn("gap-5 p-5 md:p-7.5", SIZES[size], className),
58
+ onFocusCapture: (event) => {
59
+ onFocusCapture?.(event);
60
+ if (event.target instanceof HTMLElement && event.target.closest(`[data-component="${QUESTION_NAME}"]`) === null) lastFocusRef.current = event.target;
61
+ },
62
+ onEscapeKeyDown: (event) => {
63
+ onEscapeKeyDown?.(event);
64
+ if (asking && !event.defaultPrevented) {
65
+ event.preventDefault();
66
+ keepRef.current?.();
67
+ }
68
+ },
69
+ ...props,
70
+ children: /* @__PURE__ */ jsx(ActionDialogProvider, {
71
+ value: context,
72
+ children
73
+ })
74
+ });
75
+ });
76
+ ActionDialogContent.displayName = CONTENT_NAME;
77
+ const BODY_NAME = "ActionDialog.Body";
78
+ const ActionDialogBody = forwardRef(({ className, ...props }, ref) => {
79
+ const { asking } = useActionDialog();
80
+ return /* @__PURE__ */ jsx(Dialog.Body, {
81
+ ref,
82
+ "data-component": BODY_NAME,
83
+ className: cn("-mx-2 flex flex-col gap-7 px-2 py-1 transition-opacity", asking && "opacity-50", className),
84
+ ...props,
85
+ inert: asking || props.inert
86
+ });
87
+ });
88
+ ActionDialogBody.displayName = BODY_NAME;
89
+ const ACTION_NAME = "ActionDialog.Action";
90
+ /** An action of the footer: enabled by the content, red for a destructive one. */
91
+ const ActionDialogAction = forwardRef(({ label, intent = "default", disabled = false, closeOnClick = true, variant = "solid", className, onClick, ...props }, ref) => {
92
+ const { confirmEnabled } = useActionDialog();
93
+ const root = useActionDialogRoot();
94
+ const button = /* @__PURE__ */ jsx(Button, {
95
+ ref,
96
+ "data-component": ACTION_NAME,
97
+ variant,
98
+ size: "lg",
99
+ label,
100
+ disabled: disabled || !confirmEnabled,
101
+ className: cn(intent === "danger" && DANGER_BUTTON_CLASS_NAME, className),
102
+ onClick: (event) => {
103
+ onClick?.(event);
104
+ if (closeOnClick && root !== void 0 && !event.defaultPrevented) root.actionCloseRef.current = true;
105
+ },
106
+ ...props
107
+ });
108
+ return closeOnClick ? /* @__PURE__ */ jsx(Dialog.Close, {
109
+ asChild: true,
110
+ children: button
111
+ }) : button;
112
+ });
113
+ ActionDialogAction.displayName = ACTION_NAME;
114
+ const FOOTER_NAME = "ActionDialog.Footer";
115
+ /**
116
+ * The footer: Cancel and Confirm, or the controls it is given. While it asks a question, every
117
+ * control hides — hidden, not unmounted, so the one the focus returns to is still there.
118
+ */
119
+ const ActionDialogFooter = forwardRef(({ onConfirm, onCancel, confirmLabel, cancelLabel, intent = "default", error, confirmRef, cancelVariant = "text", closeOnCancel = true, closeOnConfirm = true, question, className, children, ...props }, ref) => {
120
+ const t = useUiKitPhrases();
121
+ const { setAsking, keepRef } = useActionDialog();
122
+ const asking = question !== void 0;
123
+ const keepButtonRef = useRef(null);
124
+ const questionId = usePrefixedId();
125
+ keepRef.current = question?.onKeep;
126
+ useLayoutEffect(() => {
127
+ setAsking(asking);
128
+ return () => setAsking(false);
129
+ }, [asking, setAsking]);
130
+ useEffect(() => {
131
+ if (asking) keepButtonRef.current?.focus();
132
+ }, [asking]);
133
+ const cancel = /* @__PURE__ */ jsx(Button, {
134
+ variant: cancelVariant,
135
+ size: "lg",
136
+ label: cancelLabel ?? t("enonic.uiKit.dialog.cancel"),
137
+ onClick: onCancel
138
+ });
139
+ return /* @__PURE__ */ jsxs(Dialog.Footer, {
140
+ ref,
141
+ "data-component": FOOTER_NAME,
142
+ className: cn("items-center", className),
143
+ ...props,
144
+ children: [question !== void 0 && /* @__PURE__ */ jsxs("div", {
145
+ "data-component": QUESTION_NAME,
146
+ className: "flex flex-1 flex-wrap items-center justify-end gap-x-5 gap-y-2.5",
147
+ children: [/* @__PURE__ */ jsxs("div", {
148
+ id: questionId,
149
+ role: "alert",
150
+ className: "mr-auto flex items-center gap-2 text-sm",
151
+ children: [/* @__PURE__ */ jsx(TriangleAlert, { className: cn("size-4 shrink-0", question.intent === "danger" ? "text-error" : "text-subtle") }), question.text]
152
+ }), /* @__PURE__ */ jsxs("div", {
153
+ className: "flex shrink-0 gap-2.5",
154
+ children: [/* @__PURE__ */ jsx(Button, {
155
+ ref: keepButtonRef,
156
+ "aria-describedby": questionId,
157
+ variant: "outline",
158
+ size: "lg",
159
+ label: question.keepLabel ?? t("enonic.uiKit.dialog.keep"),
160
+ onClick: question.onKeep
161
+ }), /* @__PURE__ */ jsx(Button, {
162
+ "aria-describedby": questionId,
163
+ variant: "solid",
164
+ size: "lg",
165
+ label: question.confirmLabel ?? t("enonic.uiKit.dialog.confirm"),
166
+ className: cn(question.intent === "danger" && DANGER_BUTTON_CLASS_NAME),
167
+ onClick: question.onConfirm
168
+ })]
169
+ })]
170
+ }), /* @__PURE__ */ jsxs("div", {
171
+ className: cn("flex flex-1 items-center justify-end gap-2.5", asking && "hidden"),
172
+ children: [
173
+ error !== void 0 && /* @__PURE__ */ jsx("p", {
174
+ className: "text-error mr-auto text-sm",
175
+ role: "alert",
176
+ children: error
177
+ }),
178
+ children,
179
+ onConfirm !== void 0 && (closeOnCancel ? /* @__PURE__ */ jsx(Dialog.Close, {
180
+ asChild: true,
181
+ children: cancel
182
+ }) : cancel),
183
+ onConfirm !== void 0 && /* @__PURE__ */ jsx(ActionDialogAction, {
184
+ ref: confirmRef,
185
+ label: confirmLabel ?? t("enonic.uiKit.dialog.confirm"),
186
+ intent,
187
+ closeOnClick: closeOnConfirm,
188
+ onClick: onConfirm
189
+ })
190
+ ]
191
+ })]
192
+ });
193
+ });
194
+ ActionDialogFooter.displayName = FOOTER_NAME;
195
+ /**
196
+ * `Dialog` from `@enonic/ui` plus an outcome: a `Content` that holds whether the action may be
197
+ * taken, a `Body` that goes inert while the footer asks a question, a `Footer` that takes the
198
+ * action or asks the question, and `Action` for a further button the content enables. Stacks
199
+ * nothing: a question goes to the footer, content with room of its own is another `Content` under
200
+ * the same `Root`.
201
+ */
202
+ const ActionDialog = {
203
+ ...Dialog,
204
+ Root: ActionDialogRoot,
205
+ Content: ActionDialogContent,
206
+ Body: ActionDialogBody,
207
+ Footer: ActionDialogFooter,
208
+ Action: ActionDialogAction
209
+ };
210
+ //#endregion
211
+ export { ActionDialog };
212
+
213
+ //# sourceMappingURL=action-dialog.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"action-dialog.js","names":[],"sources":["../../src/dialog/action-dialog.tsx"],"sourcesContent":["import {\n Button,\n type ButtonVariant,\n cn,\n Dialog,\n type DialogRootProps,\n usePrefixedId,\n} from '@enonic/ui';\nimport { TriangleAlert } from 'lucide-react';\nimport {\n type ComponentPropsWithoutRef,\n forwardRef,\n type ReactElement,\n type ReactNode,\n type Ref,\n useEffect,\n useLayoutEffect,\n useMemo,\n useRef,\n useState,\n} from 'react';\n\nimport { useUiKitPhrases } from '../i18n/use-phrases';\nimport {\n type ActionDialogContextValue,\n type ActionDialogOpenChangeDetails,\n ActionDialogProvider,\n ActionDialogRootProvider,\n useActionDialog,\n useActionDialogRoot,\n} from './action-dialog-context';\n\nexport type DialogIntent = 'default' | 'danger';\n\n/** A question needs no more room than its own text; `medium` carries a control, `wide` a form. */\nexport type DialogSize = 'default' | 'medium' | 'wide';\n\nconst SIZES: Record<DialogSize, string> = {\n default: 'max-w-lg',\n medium: 'max-w-180 sm:min-w-152',\n wide: 'max-w-4xl',\n};\n\nconst DANGER_BUTTON_CLASS_NAME =\n 'bg-btn-error text-alt hover:bg-btn-error-hover active:bg-btn-error-active focus-visible:ring-error/50';\n\n//\n// * Root\n//\n\nexport type ActionDialogRootProps = Omit<DialogRootProps, 'onOpenChange'> & {\n /** `details` tells a close an action took from one that would leave without it. */\n onOpenChange?: (open: boolean, details?: ActionDialogOpenChangeDetails) => void;\n};\n\nconst ActionDialogRoot = ({ onOpenChange, ...props }: ActionDialogRootProps): ReactElement => {\n const actionCloseRef = useRef(false);\n const value = useMemo(() => ({ actionCloseRef }), []);\n\n return (\n <ActionDialogRootProvider value={value}>\n <Dialog.Root\n {...props}\n onOpenChange={(open) => {\n const byAction = actionCloseRef.current;\n actionCloseRef.current = false;\n onOpenChange?.(open, !open && byAction ? { reason: 'action' } : undefined);\n }}\n />\n </ActionDialogRootProvider>\n );\n};\nActionDialogRoot.displayName = 'ActionDialog.Root';\n\n//\n// * Content\n//\n\nexport type ActionDialogContentProps = {\n size?: DialogSize;\n /** Whether the action starts enabled; a content with a gate starts it disabled. */\n defaultConfirmEnabled?: boolean;\n} & ComponentPropsWithoutRef<typeof Dialog.Content>;\n\nconst CONTENT_NAME = 'ActionDialog.Content';\nconst QUESTION_NAME = 'ActionDialog.Question';\n\nconst ActionDialogContent = forwardRef<HTMLDivElement, ActionDialogContentProps>(\n (\n {\n size = 'default',\n defaultConfirmEnabled = true,\n className,\n children,\n onEscapeKeyDown,\n onFocusCapture,\n ...props\n },\n ref,\n ): ReactElement => {\n const [confirmEnabled, setConfirmEnabled] = useState(defaultConfirmEnabled);\n const [asking, setAsking] = useState(false);\n const keepRef = useRef<(() => void) | undefined>(undefined);\n const lastFocusRef = useRef<HTMLElement | null>(null);\n const wasAskingRef = useRef(false);\n\n const context = useMemo<ActionDialogContextValue>(\n () => ({ confirmEnabled, setConfirmEnabled, asking, setAsking, keepRef, lastFocusRef }),\n [confirmEnabled, asking],\n );\n\n // Answered, the focus goes back to the control the question left. The last focus, not the active\n // element: a mask click has already blurred the field to the body.\n useLayoutEffect(() => {\n const target = lastFocusRef.current;\n const answered = wasAskingRef.current && !asking;\n wasAskingRef.current = asking;\n if (!answered || target === null) {\n return;\n }\n // ! A frame later: under Preact the body reads `asking` through context and lifts its `inert`\n // ! in a render after this one, and an inert field takes no focus.\n const frame = requestAnimationFrame(() => target.focus());\n return () => cancelAnimationFrame(frame);\n }, [asking]);\n\n return (\n <Dialog.Content\n ref={ref}\n data-component={CONTENT_NAME}\n className={cn('gap-5 p-5 md:p-7.5', SIZES[size], className)}\n onFocusCapture={(event) => {\n onFocusCapture?.(event);\n if (\n event.target instanceof HTMLElement &&\n event.target.closest(`[data-component=\"${QUESTION_NAME}\"]`) === null\n ) {\n lastFocusRef.current = event.target;\n }\n }}\n onEscapeKeyDown={(event) => {\n onEscapeKeyDown?.(event);\n // While the footer asks its question, `Escape` is the way back, not the way out.\n if (asking && !event.defaultPrevented) {\n event.preventDefault();\n keepRef.current?.();\n }\n }}\n {...props}\n >\n <ActionDialogProvider value={context}>{children}</ActionDialogProvider>\n </Dialog.Content>\n );\n },\n);\nActionDialogContent.displayName = CONTENT_NAME;\n\n//\n// * Body\n//\n\nexport type ActionDialogBodyProps = ComponentPropsWithoutRef<typeof Dialog.Body>;\n\nconst BODY_NAME = 'ActionDialog.Body';\n\nconst ActionDialogBody = forwardRef<HTMLDivElement, ActionDialogBodyProps>(\n ({ className, ...props }, ref): ReactElement => {\n const { asking } = useActionDialog();\n\n return (\n <Dialog.Body\n ref={ref}\n data-component={BODY_NAME}\n className={cn(\n '-mx-2 flex flex-col gap-7 px-2 py-1 transition-opacity',\n asking && 'opacity-50',\n className,\n )}\n {...props}\n // ! `inert`, not a disabled overlay: the form stays in sight but takes no click, no focus.\n inert={asking || props.inert}\n />\n );\n },\n);\nActionDialogBody.displayName = BODY_NAME;\n\n//\n// * Action — a button that takes the dialog's outcome\n//\n\nexport type ActionDialogActionProps = {\n label: string;\n /** `danger` paints the button red, for an action there is no undoing. */\n intent?: DialogIntent;\n /** Enabled while the content allows the action — a gate matched, a form valid — unless disabled outright. */\n disabled?: boolean;\n /** Close the dialog on click; off for a caller that moves on to a progress view instead. */\n closeOnClick?: boolean;\n variant?: ButtonVariant;\n} & Omit<ComponentPropsWithoutRef<typeof Button>, 'label' | 'variant' | 'disabled'>;\n\nconst ACTION_NAME = 'ActionDialog.Action';\n\n/** An action of the footer: enabled by the content, red for a destructive one. */\nconst ActionDialogAction = forwardRef<HTMLButtonElement, ActionDialogActionProps>(\n (\n {\n label,\n intent = 'default',\n disabled = false,\n closeOnClick = true,\n variant = 'solid',\n className,\n onClick,\n ...props\n },\n ref,\n ): ReactElement => {\n const { confirmEnabled } = useActionDialog();\n const root = useActionDialogRoot();\n const button = (\n <Button\n ref={ref}\n data-component={ACTION_NAME}\n variant={variant}\n size=\"lg\"\n label={label}\n disabled={disabled || !confirmEnabled}\n className={cn(intent === 'danger' && DANGER_BUTTON_CLASS_NAME, className)}\n onClick={(event) => {\n onClick?.(event);\n // ! Runs before `Dialog.Close` closes: the close guard reads this to let the action through.\n if (closeOnClick && root !== undefined && !event.defaultPrevented) {\n root.actionCloseRef.current = true;\n }\n }}\n {...props}\n />\n );\n\n return closeOnClick ? <Dialog.Close asChild>{button}</Dialog.Close> : button;\n },\n);\nActionDialogAction.displayName = ACTION_NAME;\n\n//\n// * Footer\n//\n\n/** A question the footer asks in place of its controls. */\nexport type FooterQuestion = {\n text: ReactNode;\n intent?: DialogIntent;\n /** Defaults to the kit's \"Confirm\". */\n confirmLabel?: string;\n /** The way back; defaults to the kit's \"Keep editing\". */\n keepLabel?: string;\n onConfirm: () => void;\n onKeep: () => void;\n};\n\nexport type ActionDialogFooterProps = {\n /** Given `onConfirm`, the footer renders Cancel and Confirm; without it, its children are the controls. */\n onConfirm?: () => void;\n onCancel?: () => void;\n /** Defaults to the kit's \"Confirm\". */\n confirmLabel?: string;\n /** Defaults to the kit's \"Cancel\". */\n cancelLabel?: string;\n /** `danger` paints the confirm button red, for an action there is no undoing. */\n intent?: DialogIntent;\n /** Why the dialog is still open, shown beside its controls — a rejected save is the case. */\n error?: string;\n /** For a caller that moves the focus onto the confirm button once it means something. */\n confirmRef?: Ref<HTMLButtonElement>;\n /** `outline` gives the two answers of a question equal weight; a form's Cancel stays quiet. */\n cancelVariant?: ButtonVariant;\n /** Close the dialog on Cancel; off for a caller that closes it itself. */\n closeOnCancel?: boolean;\n /** Close the dialog on Confirm; off for a caller that moves on to a progress view instead. */\n closeOnConfirm?: boolean;\n /**\n * While set, the footer asks this instead of showing its controls: the body goes inert, `Escape`\n * is `onKeep`, and the focus goes to the way back. `useCloseGuard` is the usual source.\n */\n question?: FooterQuestion;\n} & ComponentPropsWithoutRef<typeof Dialog.Footer>;\n\nconst FOOTER_NAME = 'ActionDialog.Footer';\n\n/**\n * The footer: Cancel and Confirm, or the controls it is given. While it asks a question, every\n * control hides — hidden, not unmounted, so the one the focus returns to is still there.\n */\nconst ActionDialogFooter = forwardRef<HTMLElement, ActionDialogFooterProps>(\n (\n {\n onConfirm,\n onCancel,\n confirmLabel,\n cancelLabel,\n intent = 'default',\n error,\n confirmRef,\n cancelVariant = 'text',\n closeOnCancel = true,\n closeOnConfirm = true,\n question,\n className,\n children,\n ...props\n },\n ref,\n ): ReactElement => {\n const t = useUiKitPhrases();\n const { setAsking, keepRef } = useActionDialog();\n const asking = question !== undefined;\n const keepButtonRef = useRef<HTMLButtonElement>(null);\n const questionId = usePrefixedId();\n\n keepRef.current = question?.onKeep;\n\n useLayoutEffect(() => {\n setAsking(asking);\n return () => setAsking(false);\n }, [asking, setAsking]);\n\n useEffect(() => {\n if (asking) {\n keepButtonRef.current?.focus();\n }\n }, [asking]);\n\n const cancel = (\n <Button\n variant={cancelVariant}\n size=\"lg\"\n label={cancelLabel ?? t('enonic.uiKit.dialog.cancel')}\n onClick={onCancel}\n />\n );\n\n return (\n <Dialog.Footer\n ref={ref}\n data-component={FOOTER_NAME}\n className={cn('items-center', className)}\n {...props}\n >\n {question !== undefined && (\n <div\n data-component={QUESTION_NAME}\n className=\"flex flex-1 flex-wrap items-center justify-end gap-x-5 gap-y-2.5\"\n >\n {/* Announced as it appears, and read with either answer the focus lands on. */}\n <div id={questionId} role=\"alert\" className=\"mr-auto flex items-center gap-2 text-sm\">\n <TriangleAlert\n className={cn(\n 'size-4 shrink-0',\n question.intent === 'danger' ? 'text-error' : 'text-subtle',\n )}\n />\n {question.text}\n </div>\n <div className=\"flex shrink-0 gap-2.5\">\n <Button\n ref={keepButtonRef}\n aria-describedby={questionId}\n variant=\"outline\"\n size=\"lg\"\n label={question.keepLabel ?? t('enonic.uiKit.dialog.keep')}\n onClick={question.onKeep}\n />\n <Button\n aria-describedby={questionId}\n variant=\"solid\"\n size=\"lg\"\n label={question.confirmLabel ?? t('enonic.uiKit.dialog.confirm')}\n className={cn(question.intent === 'danger' && DANGER_BUTTON_CLASS_NAME)}\n onClick={question.onConfirm}\n />\n </div>\n </div>\n )}\n <div className={cn('flex flex-1 items-center justify-end gap-2.5', asking && 'hidden')}>\n {error !== undefined && (\n <p className=\"text-error mr-auto text-sm\" role=\"alert\">\n {error}\n </p>\n )}\n {children}\n {onConfirm !== undefined &&\n (closeOnCancel ? <Dialog.Close asChild>{cancel}</Dialog.Close> : cancel)}\n {onConfirm !== undefined && (\n <ActionDialogAction\n ref={confirmRef}\n label={confirmLabel ?? t('enonic.uiKit.dialog.confirm')}\n intent={intent}\n closeOnClick={closeOnConfirm}\n onClick={onConfirm}\n />\n )}\n </div>\n </Dialog.Footer>\n );\n },\n);\nActionDialogFooter.displayName = FOOTER_NAME;\n\n//\n// * Compound\n//\n\n/**\n * `Dialog` from `@enonic/ui` plus an outcome: a `Content` that holds whether the action may be\n * taken, a `Body` that goes inert while the footer asks a question, a `Footer` that takes the\n * action or asks the question, and `Action` for a further button the content enables. Stacks\n * nothing: a question goes to the footer, content with room of its own is another `Content` under\n * the same `Root`.\n */\nexport const ActionDialog = {\n ...Dialog,\n Root: ActionDialogRoot,\n Content: ActionDialogContent,\n Body: ActionDialogBody,\n Footer: ActionDialogFooter,\n Action: ActionDialogAction,\n};\n"],"mappings":"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"}
@@ -0,0 +1,33 @@
1
+ import { DialogSize } from "./action-dialog.js";
2
+ import { DeleteTarget } from "./delete-expectation.js";
3
+ import { ReactElement, ReactNode } from "react";
4
+ //#region src/dialog/delete-confirm.d.ts
5
+ type DeleteConfirmProps = {
6
+ title: string;
7
+ /** The question, already worded for one target or many; the targets are listed under it. */
8
+ question: ReactNode;
9
+ targets: readonly DeleteTarget[];
10
+ /**
11
+ * What is typed back before the button enables: `deleteExpectation(targets)` by default, the
12
+ * one name or the count. `null` asks for nothing, and the button is enabled from the start.
13
+ */
14
+ expected?: string | number | null;
15
+ /** Defaults to the kit's "Delete". */
16
+ confirmLabel?: string;
17
+ size?: DialogSize;
18
+ onConfirm: () => void;
19
+ onCancel?: () => void;
20
+ /** Off for a caller that moves on to a progress view instead of closing. */
21
+ closeOnConfirm?: boolean;
22
+ /** Off for a caller whose Cancel goes back to the view before — a list the delete was picked from. */
23
+ closeOnCancel?: boolean;
24
+ 'data-component'?: string;
25
+ };
26
+ declare const DELETE_CONFIRM_NAME = "DeleteConfirm";
27
+ declare function DeleteConfirm({ title, question, targets, expected, confirmLabel, size, onConfirm, onCancel, closeOnConfirm, closeOnCancel, 'data-component': componentName }: DeleteConfirmProps): ReactElement;
28
+ declare namespace DeleteConfirm {
29
+ export { DELETE_CONFIRM_NAME as displayName };
30
+ }
31
+ //#endregion
32
+ export { DeleteConfirm, DeleteConfirmProps };
33
+ //# sourceMappingURL=delete-confirm.d.ts.map