@ttt-productions/ui-core 0.11.12 → 0.11.15
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/react/components/badge.d.ts +2 -2
- package/dist/react/components/button.d.ts +11 -2
- package/dist/react/components/button.d.ts.map +1 -1
- package/dist/react/components/button.js +18 -5
- package/dist/react/components/button.js.map +1 -1
- package/dist/react/components/chunk-error-recovery.js +2 -2
- package/dist/react/components/chunk-error-recovery.js.map +1 -1
- package/dist/react/components/consequence-dialog.d.ts +1 -1
- package/dist/react/components/consequence-dialog.d.ts.map +1 -1
- package/dist/react/components/consequence-dialog.js +2 -3
- package/dist/react/components/consequence-dialog.js.map +1 -1
- package/dist/react/components/dropdown-menu.d.ts +8 -0
- package/dist/react/components/dropdown-menu.d.ts.map +1 -1
- package/dist/react/components/dropdown-menu.js +2 -1
- package/dist/react/components/dropdown-menu.js.map +1 -1
- package/dist/react/components/list-pagination.d.ts.map +1 -1
- package/dist/react/components/list-pagination.js +19 -1
- package/dist/react/components/list-pagination.js.map +1 -1
- package/dist/react/components/search-dropdown.d.ts.map +1 -1
- package/dist/react/components/search-dropdown.js +3 -2
- package/dist/react/components/search-dropdown.js.map +1 -1
- package/dist/react/components/select.d.ts +8 -1
- package/dist/react/components/select.d.ts.map +1 -1
- package/dist/react/components/select.js +2 -1
- package/dist/react/components/select.js.map +1 -1
- package/dist/react/components/spinner.d.ts +23 -0
- package/dist/react/components/spinner.d.ts.map +1 -0
- package/dist/react/components/spinner.js +27 -0
- package/dist/react/components/spinner.js.map +1 -0
- package/dist/react/components/switch.d.ts +8 -1
- package/dist/react/components/switch.d.ts.map +1 -1
- package/dist/react/components/switch.js +2 -1
- package/dist/react/components/switch.js.map +1 -1
- package/dist/react/hooks/use-async-action.d.ts +30 -0
- package/dist/react/hooks/use-async-action.d.ts.map +1 -0
- package/dist/react/hooks/use-async-action.js +39 -0
- package/dist/react/hooks/use-async-action.js.map +1 -0
- package/dist/react/hooks/use-return-scroll.d.ts +26 -0
- package/dist/react/hooks/use-return-scroll.d.ts.map +1 -0
- package/dist/react/hooks/use-return-scroll.js +123 -0
- package/dist/react/hooks/use-return-scroll.js.map +1 -0
- package/dist/react/index.d.ts +6 -0
- package/dist/react/index.d.ts.map +1 -1
- package/dist/react/index.js +6 -0
- package/dist/react/index.js.map +1 -1
- package/package.json +3 -3
- package/src/react/components/button.tsx +46 -10
- package/src/react/components/chunk-error-recovery.tsx +2 -2
- package/src/react/components/consequence-dialog.tsx +4 -6
- package/src/react/components/dropdown-menu.tsx +16 -2
- package/src/react/components/list-pagination.tsx +24 -4
- package/src/react/components/search-dropdown.tsx +4 -3
- package/src/react/components/select.tsx +19 -5
- package/src/react/components/spinner.tsx +49 -0
- package/src/react/components/switch.tsx +16 -4
- package/src/react/hooks/use-async-action.ts +63 -0
- package/src/react/hooks/use-return-scroll.ts +149 -0
- package/src/react/index.ts +11 -0
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import * as React from "react";
|
|
2
2
|
import { type VariantProps } from "class-variance-authority";
|
|
3
3
|
declare const badgeVariants: (props?: ({
|
|
4
|
-
variant?: "
|
|
5
|
-
status?: "
|
|
4
|
+
variant?: "status" | "default" | "destructive" | "outline" | "secondary" | null | undefined;
|
|
5
|
+
status?: "pending" | "completed" | "scheduled" | "in_progress" | "cancelled" | null | undefined;
|
|
6
6
|
} & import("class-variance-authority/types").ClassProp) | undefined) => string;
|
|
7
7
|
export interface BadgeProps extends React.HTMLAttributes<HTMLDivElement>, VariantProps<typeof badgeVariants> {
|
|
8
8
|
}
|
|
@@ -1,11 +1,20 @@
|
|
|
1
1
|
import * as React from "react";
|
|
2
2
|
import { type VariantProps } from "class-variance-authority";
|
|
3
3
|
declare const buttonVariants: (props?: ({
|
|
4
|
-
variant?: "default" | "destructive" | "success" | "outline" | "secondary" | "ghost" | "
|
|
5
|
-
size?: "
|
|
4
|
+
variant?: "link" | "default" | "destructive" | "success" | "outline" | "secondary" | "ghost" | "inverted" | null | undefined;
|
|
5
|
+
size?: "sm" | "lg" | "default" | "icon" | null | undefined;
|
|
6
6
|
} & import("class-variance-authority/types").ClassProp) | undefined) => string;
|
|
7
7
|
export interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement>, VariantProps<typeof buttonVariants> {
|
|
8
8
|
asChild?: boolean;
|
|
9
|
+
/**
|
|
10
|
+
* Async work this button started is in flight. The button disables itself — so
|
|
11
|
+
* neither a repeat click nor an implicit form resubmit can fire — sets
|
|
12
|
+
* `aria-busy`, and shows the canonical spinner: an icon button (`size="icon"`)
|
|
13
|
+
* swaps its icon for it; any other button shows it in place of its `icon`.
|
|
14
|
+
*/
|
|
15
|
+
pending?: boolean;
|
|
16
|
+
/** Leading icon, rendered before the label. While `pending`, the spinner takes its place. */
|
|
17
|
+
icon?: React.ReactNode;
|
|
9
18
|
}
|
|
10
19
|
declare const Button: React.ForwardRefExoticComponent<ButtonProps & React.RefAttributes<HTMLButtonElement>>;
|
|
11
20
|
export { Button, buttonVariants };
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"button.d.ts","sourceRoot":"","sources":["../../../src/react/components/button.tsx"],"names":[],"mappings":"AAEA,OAAO,KAAK,KAAK,MAAM,OAAO,CAAA;AAE9B,OAAO,EAAO,KAAK,YAAY,EAAE,MAAM,0BAA0B,CAAA;
|
|
1
|
+
{"version":3,"file":"button.d.ts","sourceRoot":"","sources":["../../../src/react/components/button.tsx"],"names":[],"mappings":"AAEA,OAAO,KAAK,KAAK,MAAM,OAAO,CAAA;AAE9B,OAAO,EAAO,KAAK,YAAY,EAAE,MAAM,0BAA0B,CAAA;AAKjE,QAAA,MAAM,cAAc;;;8EAgCnB,CAAA;AAED,MAAM,WAAW,WACf,SAAQ,KAAK,CAAC,oBAAoB,CAAC,iBAAiB,CAAC,EACnD,YAAY,CAAC,OAAO,cAAc,CAAC;IACrC,OAAO,CAAC,EAAE,OAAO,CAAA;IACjB;;;;;OAKG;IACH,OAAO,CAAC,EAAE,OAAO,CAAA;IACjB,6FAA6F;IAC7F,IAAI,CAAC,EAAE,KAAK,CAAC,SAAS,CAAA;CACvB;AAED,QAAA,MAAM,MAAM,uFAuCX,CAAA;AAID,OAAO,EAAE,MAAM,EAAE,cAAc,EAAE,CAAA"}
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
"use client";
|
|
2
|
-
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
3
3
|
import * as React from "react";
|
|
4
|
-
import { Slot } from "@radix-ui/react-slot";
|
|
4
|
+
import { Slot, Slottable } from "@radix-ui/react-slot";
|
|
5
5
|
import { cva } from "class-variance-authority";
|
|
6
6
|
import { cn } from "../../lib/utils.js";
|
|
7
|
+
import { Spinner } from "./spinner.js";
|
|
7
8
|
const buttonVariants = cva("inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-bold ring-offset-background transition-colors focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50", {
|
|
8
9
|
variants: {
|
|
9
10
|
variant: {
|
|
@@ -28,9 +29,21 @@ const buttonVariants = cva("inline-flex items-center justify-center gap-2 whites
|
|
|
28
29
|
size: "default",
|
|
29
30
|
},
|
|
30
31
|
});
|
|
31
|
-
const Button = React.forwardRef(({ className, variant, size, asChild = false, children, ...props }, ref) => {
|
|
32
|
-
const
|
|
33
|
-
|
|
32
|
+
const Button = React.forwardRef(({ className, variant, size, asChild = false, pending = false, icon, disabled, children, ...props }, ref) => {
|
|
33
|
+
const shared = {
|
|
34
|
+
...props,
|
|
35
|
+
...(pending ? { "aria-busy": true, "data-pending": "" } : {}),
|
|
36
|
+
disabled: disabled || pending || undefined,
|
|
37
|
+
className: cn(buttonVariants({ variant, size, className })),
|
|
38
|
+
};
|
|
39
|
+
const spinner = _jsx(Spinner, { size: size === "lg" ? "sm" : "xs" });
|
|
40
|
+
const leading = pending ? spinner : icon;
|
|
41
|
+
if (asChild) {
|
|
42
|
+
// Slot finds the Slottable among its DIRECT children, so the leading slot and the
|
|
43
|
+
// Slottable must be siblings here — never wrapped in a fragment.
|
|
44
|
+
return leading ? (_jsxs(Slot, { ref: ref, ...shared, children: [leading, _jsx(Slottable, { children: children })] })) : (_jsx(Slot, { ref: ref, ...shared, children: children }));
|
|
45
|
+
}
|
|
46
|
+
return (_jsx("button", { ref: ref, ...shared, children: size === "icon" ? (pending ? spinner : children) : (_jsxs(_Fragment, { children: [leading, children] })) }));
|
|
34
47
|
});
|
|
35
48
|
Button.displayName = "Button";
|
|
36
49
|
export { Button, buttonVariants };
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"button.js","sourceRoot":"","sources":["../../../src/react/components/button.tsx"],"names":[],"mappings":"AAAA,YAAY,CAAA;;AAEZ,OAAO,KAAK,KAAK,MAAM,OAAO,CAAA;AAC9B,OAAO,EAAE,IAAI,EAAE,MAAM,sBAAsB,CAAA;
|
|
1
|
+
{"version":3,"file":"button.js","sourceRoot":"","sources":["../../../src/react/components/button.tsx"],"names":[],"mappings":"AAAA,YAAY,CAAA;;AAEZ,OAAO,KAAK,KAAK,MAAM,OAAO,CAAA;AAC9B,OAAO,EAAE,IAAI,EAAE,SAAS,EAAE,MAAM,sBAAsB,CAAA;AACtD,OAAO,EAAE,GAAG,EAAqB,MAAM,0BAA0B,CAAA;AAEjE,OAAO,EAAE,EAAE,EAAE,MAAM,oBAAoB,CAAA;AACvC,OAAO,EAAE,OAAO,EAAE,MAAM,cAAc,CAAA;AAEtC,MAAM,cAAc,GAAG,GAAG,CACxB,8RAA8R,EAC9R;IACE,QAAQ,EAAE;QACR,OAAO,EAAE;YACP,OAAO,EACL,wHAAwH;YAC1H,WAAW,EACT,qHAAqH;YACvH,OAAO,EACL,gHAAgH;YAClH,OAAO,EACL,mGAAmG;YACrG,SAAS,EACP,qFAAqF;YACvF,KAAK,EAAE,8CAA8C;YACrD,IAAI,EAAE,iDAAiD;YACvD,QAAQ,EACN,yKAAyK;SAC5K;QACD,IAAI,EAAE;YACJ,OAAO,EAAE,gBAAgB;YACzB,EAAE,EAAE,qBAAqB;YACzB,EAAE,EAAE,sBAAsB;YAC1B,IAAI,EAAE,SAAS;SAChB;KACF;IACD,eAAe,EAAE;QACf,OAAO,EAAE,SAAS;QAClB,IAAI,EAAE,SAAS;KAChB;CACF,CACF,CAAA;AAiBD,MAAM,MAAM,GAAG,KAAK,CAAC,UAAU,CAC7B,CAAC,EAAE,SAAS,EAAE,OAAO,EAAE,IAAI,EAAE,OAAO,GAAG,KAAK,EAAE,OAAO,GAAG,KAAK,EAAE,IAAI,EAAE,QAAQ,EAAE,QAAQ,EAAE,GAAG,KAAK,EAAE,EAAE,GAAG,EAAE,EAAE;IAC1G,MAAM,MAAM,GAAG;QACb,GAAG,KAAK;QACR,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,EAAE,WAAW,EAAE,IAAI,EAAE,cAAc,EAAE,EAAE,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;QAC7D,QAAQ,EAAE,QAAQ,IAAI,OAAO,IAAI,SAAS;QAC1C,SAAS,EAAE,EAAE,CAAC,cAAc,CAAC,EAAE,OAAO,EAAE,IAAI,EAAE,SAAS,EAAE,CAAC,CAAC;KAC5D,CAAA;IACD,MAAM,OAAO,GAAG,KAAC,OAAO,IAAC,IAAI,EAAE,IAAI,KAAK,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI,GAAI,CAAA;IAC9D,MAAM,OAAO,GAAG,OAAO,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,IAAI,CAAA;IAExC,IAAI,OAAO,EAAE,CAAC;QACZ,kFAAkF;QAClF,iEAAiE;QACjE,OAAO,OAAO,CAAC,CAAC,CAAC,CACf,MAAC,IAAI,IAAC,GAAG,EAAE,GAAG,KAAM,MAAM,aACvB,OAAO,EACR,KAAC,SAAS,cAAE,QAAQ,GAAa,IAC5B,CACR,CAAC,CAAC,CAAC,CACF,KAAC,IAAI,IAAC,GAAG,EAAE,GAAG,KAAM,MAAM,YACvB,QAAQ,GACJ,CACR,CAAA;IACH,CAAC;IAED,OAAO,CACL,iBAAQ,GAAG,EAAE,GAAG,KAAM,MAAM,YACzB,IAAI,KAAK,MAAM,CAAC,CAAC,CAAC,CACjB,OAAO,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,QAAQ,CAC7B,CAAC,CAAC,CAAC,CACF,8BACG,OAAO,EACP,QAAQ,IACR,CACJ,GACM,CACV,CAAA;AACH,CAAC,CACF,CAAA;AAED,MAAM,CAAC,WAAW,GAAG,QAAQ,CAAA;AAE7B,OAAO,EAAE,MAAM,EAAE,cAAc,EAAE,CAAA","sourcesContent":["\"use client\"\n\nimport * as React from \"react\"\nimport { Slot, Slottable } from \"@radix-ui/react-slot\"\nimport { cva, type VariantProps } from \"class-variance-authority\"\n\nimport { cn } from \"../../lib/utils.js\"\nimport { Spinner } from \"./spinner.js\"\n\nconst buttonVariants = cva(\n \"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-bold ring-offset-background transition-colors focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50\",\n {\n variants: {\n variant: {\n default:\n \"button-default bg-primary text-primary-foreground hover:bg-primary/90 border-2 border-[hsl(var(--brand-primary-deep))]\",\n destructive:\n \"bg-destructive text-destructive-foreground hover:bg-destructive/90 border-2 border-[hsl(var(--brand-primary-deep))]\",\n success:\n \"bg-green-500 text-primary-foreground hover:bg-green-500/90 border-2 border-[hsl(var(--status-success-border))]\",\n outline:\n \"border-2 border-border bg-background hover:bg-accent hover:text-accent-foreground text-foreground\",\n secondary:\n \"bg-secondary text-secondary-foreground hover:bg-secondary/80 border-2 border-border\",\n ghost: \"hover:bg-accent hover:text-accent-foreground\",\n link: \"text-primary underline-offset-4 hover:underline\",\n inverted:\n \"bg-[hsl(var(--inverted-background))] text-[hsl(var(--inverted-foreground))] hover:bg-[hsl(var(--inverted-background))]/90 border-2 border-[hsl(var(--inverted-border))]\",\n },\n size: {\n default: \"h-10 px-4 py-2\",\n sm: \"h-9 rounded-md px-3\",\n lg: \"h-11 rounded-md px-8\",\n icon: \"h-9 w-9\",\n },\n },\n defaultVariants: {\n variant: \"default\",\n size: \"default\",\n },\n }\n)\n\nexport interface ButtonProps\n extends React.ButtonHTMLAttributes<HTMLButtonElement>,\n VariantProps<typeof buttonVariants> {\n asChild?: boolean\n /**\n * Async work this button started is in flight. The button disables itself — so\n * neither a repeat click nor an implicit form resubmit can fire — sets\n * `aria-busy`, and shows the canonical spinner: an icon button (`size=\"icon\"`)\n * swaps its icon for it; any other button shows it in place of its `icon`.\n */\n pending?: boolean\n /** Leading icon, rendered before the label. While `pending`, the spinner takes its place. */\n icon?: React.ReactNode\n}\n\nconst Button = React.forwardRef<HTMLButtonElement, ButtonProps>(\n ({ className, variant, size, asChild = false, pending = false, icon, disabled, children, ...props }, ref) => {\n const shared = {\n ...props,\n ...(pending ? { \"aria-busy\": true, \"data-pending\": \"\" } : {}),\n disabled: disabled || pending || undefined,\n className: cn(buttonVariants({ variant, size, className })),\n }\n const spinner = <Spinner size={size === \"lg\" ? \"sm\" : \"xs\"} />\n const leading = pending ? spinner : icon\n\n if (asChild) {\n // Slot finds the Slottable among its DIRECT children, so the leading slot and the\n // Slottable must be siblings here — never wrapped in a fragment.\n return leading ? (\n <Slot ref={ref} {...shared}>\n {leading}\n <Slottable>{children}</Slottable>\n </Slot>\n ) : (\n <Slot ref={ref} {...shared}>\n {children}\n </Slot>\n )\n }\n\n return (\n <button ref={ref} {...shared}>\n {size === \"icon\" ? (\n pending ? spinner : children\n ) : (\n <>\n {leading}\n {children}\n </>\n )}\n </button>\n )\n }\n)\n\nButton.displayName = \"Button\"\n\nexport { Button, buttonVariants }\n"]}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
3
3
|
import * as React from "react";
|
|
4
|
-
import {
|
|
4
|
+
import { Spinner } from "./spinner.js";
|
|
5
5
|
function defaultIsChunkError(event) {
|
|
6
6
|
const msg = event.message ?? "";
|
|
7
7
|
if (msg.includes("Failed to load chunk"))
|
|
@@ -27,7 +27,7 @@ export function ChunkErrorRecovery({ children, loadingMessage = "Updating…", i
|
|
|
27
27
|
return () => window.removeEventListener("error", handler);
|
|
28
28
|
}, [isChunkError]);
|
|
29
29
|
if (hasError) {
|
|
30
|
-
return (_jsxs("div", { className: "flex flex-col h-screen w-screen items-center justify-center gap-4 bg-background", children: [_jsx(
|
|
30
|
+
return (_jsxs("div", { className: "flex flex-col h-screen w-screen items-center justify-center gap-4 bg-background", children: [_jsx(Spinner, { size: "xl" }), _jsx("p", { className: "text-muted-foreground", children: loadingMessage })] }));
|
|
31
31
|
}
|
|
32
32
|
return _jsx(_Fragment, { children: children });
|
|
33
33
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"chunk-error-recovery.js","sourceRoot":"","sources":["../../../src/react/components/chunk-error-recovery.tsx"],"names":[],"mappings":"AAAA,YAAY,CAAC;;AAEb,OAAO,KAAK,KAAK,MAAM,OAAO,CAAC;AAC/B,OAAO,EAAE,OAAO,EAAE,MAAM,cAAc,CAAC;AAavC,SAAS,mBAAmB,CAAC,KAAiB;IAC5C,MAAM,GAAG,GAAG,KAAK,CAAC,OAAO,IAAI,EAAE,CAAC;IAChC,IAAI,GAAG,CAAC,QAAQ,CAAC,sBAAsB,CAAC;QAAE,OAAO,IAAI,CAAC;IACtD,MAAM,MAAM,GACV,KAAK,CAAC,KAAK,IAAI,OAAO,KAAK,CAAC,KAAK,KAAK,QAAQ;QAC5C,CAAC,CAAE,KAAK,CAAC,KAA8B,CAAC,OAAO;QAC/C,CAAC,CAAC,SAAS,CAAC;IAChB,OAAO,OAAO,MAAM,KAAK,QAAQ,IAAI,MAAM,CAAC,QAAQ,CAAC,sBAAsB,CAAC,CAAC;AAC/E,CAAC;AAED,MAAM,UAAU,kBAAkB,CAAC,EACjC,QAAQ,EACR,cAAc,GAAG,WAAW,EAC5B,YAAY,GAAG,mBAAmB,GACV;IACxB,MAAM,CAAC,QAAQ,EAAE,WAAW,CAAC,GAAG,KAAK,CAAC,QAAQ,CAAC,KAAK,CAAC,CAAC;IAEtD,KAAK,CAAC,SAAS,CAAC,GAAG,EAAE;QACnB,MAAM,OAAO,GAAG,CAAC,KAAiB,EAAE,EAAE;YACpC,IAAI,CAAC,YAAY,CAAC,KAAK,CAAC;gBAAE,OAAO;YAEjC,OAAO,CAAC,KAAK,CAAC,mCAAmC,CAAC,CAAC;YACnD,WAAW,CAAC,IAAI,CAAC,CAAC;YAClB,MAAM,CAAC,QAAQ,CAAC,MAAM,EAAE,CAAC;YACzB,KAAK,CAAC,cAAc,EAAE,CAAC;YACvB,KAAK,CAAC,eAAe,EAAE,CAAC;QAC1B,CAAC,CAAC;QACF,MAAM,CAAC,gBAAgB,CAAC,OAAO,EAAE,OAAO,CAAC,CAAC;QAC1C,OAAO,GAAG,EAAE,CAAC,MAAM,CAAC,mBAAmB,CAAC,OAAO,EAAE,OAAO,CAAC,CAAC;IAC5D,CAAC,EAAE,CAAC,YAAY,CAAC,CAAC,CAAC;IAEnB,IAAI,QAAQ,EAAE,CAAC;QACb,OAAO,CACL,eAAK,SAAS,EAAC,iFAAiF,aAC9F,KAAC,OAAO,IAAC,
|
|
1
|
+
{"version":3,"file":"chunk-error-recovery.js","sourceRoot":"","sources":["../../../src/react/components/chunk-error-recovery.tsx"],"names":[],"mappings":"AAAA,YAAY,CAAC;;AAEb,OAAO,KAAK,KAAK,MAAM,OAAO,CAAC;AAC/B,OAAO,EAAE,OAAO,EAAE,MAAM,cAAc,CAAC;AAavC,SAAS,mBAAmB,CAAC,KAAiB;IAC5C,MAAM,GAAG,GAAG,KAAK,CAAC,OAAO,IAAI,EAAE,CAAC;IAChC,IAAI,GAAG,CAAC,QAAQ,CAAC,sBAAsB,CAAC;QAAE,OAAO,IAAI,CAAC;IACtD,MAAM,MAAM,GACV,KAAK,CAAC,KAAK,IAAI,OAAO,KAAK,CAAC,KAAK,KAAK,QAAQ;QAC5C,CAAC,CAAE,KAAK,CAAC,KAA8B,CAAC,OAAO;QAC/C,CAAC,CAAC,SAAS,CAAC;IAChB,OAAO,OAAO,MAAM,KAAK,QAAQ,IAAI,MAAM,CAAC,QAAQ,CAAC,sBAAsB,CAAC,CAAC;AAC/E,CAAC;AAED,MAAM,UAAU,kBAAkB,CAAC,EACjC,QAAQ,EACR,cAAc,GAAG,WAAW,EAC5B,YAAY,GAAG,mBAAmB,GACV;IACxB,MAAM,CAAC,QAAQ,EAAE,WAAW,CAAC,GAAG,KAAK,CAAC,QAAQ,CAAC,KAAK,CAAC,CAAC;IAEtD,KAAK,CAAC,SAAS,CAAC,GAAG,EAAE;QACnB,MAAM,OAAO,GAAG,CAAC,KAAiB,EAAE,EAAE;YACpC,IAAI,CAAC,YAAY,CAAC,KAAK,CAAC;gBAAE,OAAO;YAEjC,OAAO,CAAC,KAAK,CAAC,mCAAmC,CAAC,CAAC;YACnD,WAAW,CAAC,IAAI,CAAC,CAAC;YAClB,MAAM,CAAC,QAAQ,CAAC,MAAM,EAAE,CAAC;YACzB,KAAK,CAAC,cAAc,EAAE,CAAC;YACvB,KAAK,CAAC,eAAe,EAAE,CAAC;QAC1B,CAAC,CAAC;QACF,MAAM,CAAC,gBAAgB,CAAC,OAAO,EAAE,OAAO,CAAC,CAAC;QAC1C,OAAO,GAAG,EAAE,CAAC,MAAM,CAAC,mBAAmB,CAAC,OAAO,EAAE,OAAO,CAAC,CAAC;IAC5D,CAAC,EAAE,CAAC,YAAY,CAAC,CAAC,CAAC;IAEnB,IAAI,QAAQ,EAAE,CAAC;QACb,OAAO,CACL,eAAK,SAAS,EAAC,iFAAiF,aAC9F,KAAC,OAAO,IAAC,IAAI,EAAC,IAAI,GAAG,EACrB,YAAG,SAAS,EAAC,uBAAuB,YAAE,cAAc,GAAK,IACrD,CACP,CAAC;IACJ,CAAC;IACD,OAAO,4BAAG,QAAQ,GAAI,CAAC;AACzB,CAAC","sourcesContent":["\"use client\";\n\nimport * as React from \"react\";\nimport { Spinner } from \"./spinner.js\";\n\nexport interface ChunkErrorRecoveryProps {\n children: React.ReactNode;\n /** Custom message shown during recovery reload. */\n loadingMessage?: React.ReactNode;\n /**\n * Predicate to decide whether an error event indicates a stale-chunk failure.\n * Defaults to matching \"Failed to load chunk\" in the message.\n */\n isChunkError?: (event: ErrorEvent) => boolean;\n}\n\nfunction defaultIsChunkError(event: ErrorEvent): boolean {\n const msg = event.message ?? \"\";\n if (msg.includes(\"Failed to load chunk\")) return true;\n const nested =\n event.error && typeof event.error === \"object\"\n ? (event.error as { message?: string }).message\n : undefined;\n return typeof nested === \"string\" && nested.includes(\"Failed to load chunk\");\n}\n\nexport function ChunkErrorRecovery({\n children,\n loadingMessage = \"Updating…\",\n isChunkError = defaultIsChunkError,\n}: ChunkErrorRecoveryProps) {\n const [hasError, setHasError] = React.useState(false);\n\n React.useEffect(() => {\n const handler = (event: ErrorEvent) => {\n if (!isChunkError(event)) return;\n \n console.error(\"Stale chunk detected — reloading.\");\n setHasError(true);\n window.location.reload();\n event.preventDefault();\n event.stopPropagation();\n };\n window.addEventListener(\"error\", handler);\n return () => window.removeEventListener(\"error\", handler);\n }, [isChunkError]);\n\n if (hasError) {\n return (\n <div className=\"flex flex-col h-screen w-screen items-center justify-center gap-4 bg-background\">\n <Spinner size=\"xl\" />\n <p className=\"text-muted-foreground\">{loadingMessage}</p>\n </div>\n );\n }\n return <>{children}</>;\n}\n"]}
|
|
@@ -46,7 +46,7 @@ export interface ConsequenceDialogProps {
|
|
|
46
46
|
destructive?: boolean;
|
|
47
47
|
/**
|
|
48
48
|
* Runs on confirm. May be async — while it is pending the dialog STAYS OPEN, both buttons disable,
|
|
49
|
-
* and
|
|
49
|
+
* and the confirm button shows its pending spinner. Closes on resolve. If it rejects the dialog
|
|
50
50
|
* stays open so the caller's own surface (error toast, TOTP step-up) can show and the operator can
|
|
51
51
|
* retry or cancel. This component takes no Sentry/monitoring dependency — every error path
|
|
52
52
|
* belongs to the caller's `onConfirm`.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"consequence-dialog.d.ts","sourceRoot":"","sources":["../../../src/react/components/consequence-dialog.tsx"],"names":[],"mappings":"AAkBA;;;;;GAKG;AACH,MAAM,WAAW,uBAAuB;IACtC,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,KAAK,CAAC,EAAE,KAAK,CAAC,SAAS,CAAC;IACxB,KAAK,EAAE,MAAM,CAAC;IACd,QAAQ,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;IAClC,IAAI,CAAC,EAAE,MAAM,CAAC;CACf;AAED;;;GAGG;AACH,MAAM,WAAW,kCAAkC;IACjD,MAAM,EAAE,MAAM,CAAC;IACf,KAAK,CAAC,EAAE,KAAK,CAAC,SAAS,CAAC;CACzB;AAED,MAAM,WAAW,sBAAsB;IACrC,uFAAuF;IACvF,OAAO,CAAC,EAAE,KAAK,CAAC,SAAS,CAAC;IAC1B,0DAA0D;IAC1D,IAAI,CAAC,EAAE,OAAO,CAAC;IACf,YAAY,CAAC,EAAE,CAAC,IAAI,EAAE,OAAO,KAAK,IAAI,CAAC;IAEvC,6EAA6E;IAC7E,KAAK,EAAE,KAAK,CAAC,SAAS,CAAC;IAEvB,oFAAoF;IACpF,eAAe,CAAC,EAAE,KAAK,CAAC,SAAS,CAAC;IAClC,wFAAwF;IACxF,aAAa,CAAC,EAAE,KAAK,CAAC,SAAS,CAAC;IAChC,6DAA6D;IAC7D,aAAa,CAAC,EAAE,KAAK,CAAC,SAAS,CAAC;IAEhC,8CAA8C;IAC9C,MAAM,CAAC,EAAE,uBAAuB,CAAC;IACjC,sDAAsD;IACtD,iBAAiB,CAAC,EAAE,kCAAkC,CAAC;IAEvD,YAAY,EAAE,KAAK,CAAC,SAAS,CAAC;IAC9B,WAAW,CAAC,EAAE,KAAK,CAAC,SAAS,CAAC;IAC9B,8DAA8D;IAC9D,WAAW,CAAC,EAAE,OAAO,CAAC;IAEtB;;;;;;OAMG;IACH,SAAS,EAAE,MAAM,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CACvC;AA0BD;;;;;;;;;;GAUG;AACH,wBAAgB,iBAAiB,CAAC,EAChC,OAAO,EACP,IAAI,EACJ,YAAY,EACZ,KAAK,EACL,eAAe,EACf,aAAa,EACb,aAAa,EACb,MAAM,EACN,iBAAiB,EACjB,YAAY,EACZ,WAAsB,EACtB,WAAmB,EACnB,SAAS,GACV,EAAE,sBAAsB,+
|
|
1
|
+
{"version":3,"file":"consequence-dialog.d.ts","sourceRoot":"","sources":["../../../src/react/components/consequence-dialog.tsx"],"names":[],"mappings":"AAkBA;;;;;GAKG;AACH,MAAM,WAAW,uBAAuB;IACtC,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,KAAK,CAAC,EAAE,KAAK,CAAC,SAAS,CAAC;IACxB,KAAK,EAAE,MAAM,CAAC;IACd,QAAQ,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;IAClC,IAAI,CAAC,EAAE,MAAM,CAAC;CACf;AAED;;;GAGG;AACH,MAAM,WAAW,kCAAkC;IACjD,MAAM,EAAE,MAAM,CAAC;IACf,KAAK,CAAC,EAAE,KAAK,CAAC,SAAS,CAAC;CACzB;AAED,MAAM,WAAW,sBAAsB;IACrC,uFAAuF;IACvF,OAAO,CAAC,EAAE,KAAK,CAAC,SAAS,CAAC;IAC1B,0DAA0D;IAC1D,IAAI,CAAC,EAAE,OAAO,CAAC;IACf,YAAY,CAAC,EAAE,CAAC,IAAI,EAAE,OAAO,KAAK,IAAI,CAAC;IAEvC,6EAA6E;IAC7E,KAAK,EAAE,KAAK,CAAC,SAAS,CAAC;IAEvB,oFAAoF;IACpF,eAAe,CAAC,EAAE,KAAK,CAAC,SAAS,CAAC;IAClC,wFAAwF;IACxF,aAAa,CAAC,EAAE,KAAK,CAAC,SAAS,CAAC;IAChC,6DAA6D;IAC7D,aAAa,CAAC,EAAE,KAAK,CAAC,SAAS,CAAC;IAEhC,8CAA8C;IAC9C,MAAM,CAAC,EAAE,uBAAuB,CAAC;IACjC,sDAAsD;IACtD,iBAAiB,CAAC,EAAE,kCAAkC,CAAC;IAEvD,YAAY,EAAE,KAAK,CAAC,SAAS,CAAC;IAC9B,WAAW,CAAC,EAAE,KAAK,CAAC,SAAS,CAAC;IAC9B,8DAA8D;IAC9D,WAAW,CAAC,EAAE,OAAO,CAAC;IAEtB;;;;;;OAMG;IACH,SAAS,EAAE,MAAM,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CACvC;AA0BD;;;;;;;;;;GAUG;AACH,wBAAgB,iBAAiB,CAAC,EAChC,OAAO,EACP,IAAI,EACJ,YAAY,EACZ,KAAK,EACL,eAAe,EACf,aAAa,EACb,aAAa,EACb,MAAM,EACN,iBAAiB,EACjB,YAAY,EACZ,WAAsB,EACtB,WAAmB,EACnB,SAAS,GACV,EAAE,sBAAsB,+BA+IxB"}
|
|
@@ -6,7 +6,7 @@ import { Button } from './button.js';
|
|
|
6
6
|
import { Textarea } from './textarea.js';
|
|
7
7
|
import { Input } from './input.js';
|
|
8
8
|
import { Label } from './label.js';
|
|
9
|
-
import {
|
|
9
|
+
import { Zap, Clock, RotateCcw } from 'lucide-react';
|
|
10
10
|
const SLOT_ICON_CLASS = 'icon-xs mt-0.5 shrink-0 text-muted-foreground';
|
|
11
11
|
function ConsequenceRow({ icon: Icon, label, children, }) {
|
|
12
12
|
return (_jsxs("span", { className: "flex items-start gap-2", children: [_jsx(Icon, { className: SLOT_ICON_CLASS, "aria-hidden": "true" }), _jsxs("span", { className: "stack-1 min-w-0", children: [_jsx("span", { className: "text-caption font-medium uppercase tracking-wide text-muted-foreground", children: label }), _jsx("span", { className: "text-small text-foreground", children: children })] })] }));
|
|
@@ -71,10 +71,9 @@ export function ConsequenceDialog({ trigger, open, onOpenChange, title, immediat
|
|
|
71
71
|
: (reason.minLength === undefined || reasonTrimmedLength >= reason.minLength) &&
|
|
72
72
|
(reason.maxLength === undefined || reason.value.length <= reason.maxLength);
|
|
73
73
|
const typedOk = !typedConfirmation ? true : typedValue === typedConfirmation.phrase;
|
|
74
|
-
const confirmDisabled = pending || !reasonOk || !typedOk;
|
|
75
74
|
return (_jsxs(AlertDialog, { open: actualOpen, onOpenChange: handleOpenChange, children: [trigger ? _jsx(AlertDialogTrigger, { asChild: true, children: trigger }) : null, _jsxs(AlertDialogContent, { onEscapeKeyDown: (e) => {
|
|
76
75
|
if (pending)
|
|
77
76
|
e.preventDefault();
|
|
78
|
-
}, children: [_jsxs(AlertDialogHeader, { children: [_jsx(AlertDialogTitle, { children: title }), _jsx(AlertDialogDescription, { asChild: true, children: _jsxs("span", { className: "stack-3", children: [immediateEffect ? (_jsx(ConsequenceRow, { icon: Zap, label: "Immediately", children: immediateEffect })) : null, delayedEffect ? (_jsx(ConsequenceRow, { icon: Clock, label: "Afterward", children: delayedEffect })) : null, reversibility ? (_jsx(ConsequenceRow, { icon: RotateCcw, label: "Reversibility", children: reversibility })) : null] }) })] }), reason ? (_jsxs("div", { className: "stack-2", children: [_jsx(Label, { htmlFor: reasonId, children: reason.label ?? 'Reason' }), _jsx(Textarea, { id: reasonId, value: reason.value, onChange: (e) => reason.onChange(e.target.value), placeholder: reason.placeholder, maxLength: reason.maxLength, rows: reason.rows ?? 3, disabled: pending })] })) : null, typedConfirmation ? (_jsxs("div", { className: "stack-2", children: [_jsx(Label, { htmlFor: typedId, children: typedConfirmation.label ?? (_jsxs(_Fragment, { children: ["Type ", _jsx("span", { className: "font-mono", children: typedConfirmation.phrase }), " to confirm"] })) }), _jsx(Input, { id: typedId, value: typedValue, onChange: (e) => setTypedValue(e.target.value), placeholder: typedConfirmation.phrase, disabled: pending, autoComplete: "off" })] })) : null, _jsxs(AlertDialogFooter, { children: [_jsx(Button, { variant: "outline", onClick: () => handleOpenChange(false), disabled: pending, children: cancelLabel }),
|
|
77
|
+
}, children: [_jsxs(AlertDialogHeader, { children: [_jsx(AlertDialogTitle, { children: title }), _jsx(AlertDialogDescription, { asChild: true, children: _jsxs("span", { className: "stack-3", children: [immediateEffect ? (_jsx(ConsequenceRow, { icon: Zap, label: "Immediately", children: immediateEffect })) : null, delayedEffect ? (_jsx(ConsequenceRow, { icon: Clock, label: "Afterward", children: delayedEffect })) : null, reversibility ? (_jsx(ConsequenceRow, { icon: RotateCcw, label: "Reversibility", children: reversibility })) : null] }) })] }), reason ? (_jsxs("div", { className: "stack-2", children: [_jsx(Label, { htmlFor: reasonId, children: reason.label ?? 'Reason' }), _jsx(Textarea, { id: reasonId, value: reason.value, onChange: (e) => reason.onChange(e.target.value), placeholder: reason.placeholder, maxLength: reason.maxLength, rows: reason.rows ?? 3, disabled: pending })] })) : null, typedConfirmation ? (_jsxs("div", { className: "stack-2", children: [_jsx(Label, { htmlFor: typedId, children: typedConfirmation.label ?? (_jsxs(_Fragment, { children: ["Type ", _jsx("span", { className: "font-mono", children: typedConfirmation.phrase }), " to confirm"] })) }), _jsx(Input, { id: typedId, value: typedValue, onChange: (e) => setTypedValue(e.target.value), placeholder: typedConfirmation.phrase, disabled: pending, autoComplete: "off" })] })) : null, _jsxs(AlertDialogFooter, { children: [_jsx(Button, { variant: "outline", onClick: () => handleOpenChange(false), disabled: pending, children: cancelLabel }), _jsx(Button, { variant: destructive ? 'destructive' : 'default', onClick: handleConfirm, disabled: !reasonOk || !typedOk, pending: pending, children: confirmLabel })] })] })] }));
|
|
79
78
|
}
|
|
80
79
|
//# sourceMappingURL=consequence-dialog.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"consequence-dialog.js","sourceRoot":"","sources":["../../../src/react/components/consequence-dialog.tsx"],"names":[],"mappings":"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","sourcesContent":["'use client';\n\nimport { useCallback, useId, useState } from 'react';\nimport {\n AlertDialog,\n AlertDialogTrigger,\n AlertDialogContent,\n AlertDialogHeader,\n AlertDialogFooter,\n AlertDialogTitle,\n AlertDialogDescription,\n} from './alert-dialog.js';\nimport { Button } from './button.js';\nimport { Textarea } from './textarea.js';\nimport { Input } from './input.js';\nimport { Label } from './label.js';\nimport { Loader2, Zap, Clock, RotateCcw } from 'lucide-react';\n\n/**\n * The inline required-reason config. The caller OWNS the reason value/onChange (draft state lives at\n * the call site). Any `minLength`/`maxLength` the caller supplies MUST come from a caller-owned\n * constant — this component declares no business numbers and no copy; it only derives the gate + the\n * Textarea `maxLength` from the values it is handed.\n */\nexport interface ConsequenceDialogReason {\n required?: boolean;\n minLength?: number;\n maxLength?: number;\n placeholder?: string;\n label?: React.ReactNode;\n value: string;\n onChange: (value: string) => void;\n rows?: number;\n}\n\n/**\n * The exact-match typed-confirmation gate. `phrase` is business copy owned by the CALLER; the\n * component only compares the typed value against it.\n */\nexport interface ConsequenceDialogTypedConfirmation {\n phrase: string;\n label?: React.ReactNode;\n}\n\nexport interface ConsequenceDialogProps {\n /** Uncontrolled open model: the element that opens the dialog (rendered `asChild`). */\n trigger?: React.ReactNode;\n /** Controlled open model. Provide with `onOpenChange`. */\n open?: boolean;\n onOpenChange?: (open: boolean) => void;\n\n /** Heading — a string or node so callers can interpolate amounts / names. */\n title: React.ReactNode;\n\n /** SLOT 1 — what happens right now (e.g. \"initiates a Stripe refund of $X now\"). */\n immediateEffect?: React.ReactNode;\n /** SLOT 2 — what settles later, out of band (webhook / worker / async confirmation). */\n delayedEffect?: React.ReactNode;\n /** SLOT 3 — whether/how this can be undone or superseded. */\n reversibility?: React.ReactNode;\n\n /** Inline required-reason field. Optional. */\n reason?: ConsequenceDialogReason;\n /** Exact-phrase typed-confirmation gate. Optional. */\n typedConfirmation?: ConsequenceDialogTypedConfirmation;\n\n confirmLabel: React.ReactNode;\n cancelLabel?: React.ReactNode;\n /** `true` styles the confirm button with `bg-destructive`. */\n destructive?: boolean;\n\n /**\n * Runs on confirm. May be async — while it is pending the dialog STAYS OPEN, both buttons disable,\n * and a `Loader2` spins in the confirm button. Closes on resolve. If it rejects the dialog\n * stays open so the caller's own surface (error toast, TOTP step-up) can show and the operator can\n * retry or cancel. This component takes no Sentry/monitoring dependency — every error path\n * belongs to the caller's `onConfirm`.\n */\n onConfirm: () => void | Promise<void>;\n}\n\nconst SLOT_ICON_CLASS = 'icon-xs mt-0.5 shrink-0 text-muted-foreground';\n\nfunction ConsequenceRow({\n icon: Icon,\n label,\n children,\n}: {\n icon: typeof Zap;\n label: string;\n children: React.ReactNode;\n}) {\n return (\n <span className=\"flex items-start gap-2\">\n <Icon className={SLOT_ICON_CLASS} aria-hidden=\"true\" />\n <span className=\"stack-1 min-w-0\">\n <span className=\"text-caption font-medium uppercase tracking-wide text-muted-foreground\">\n {label}\n </span>\n <span className=\"text-small text-foreground\">{children}</span>\n </span>\n </span>\n );\n}\n\n/**\n * A reusable confirm-before-a-consequential-action dialog. Three optional, consistently-styled\n * consequence slots (immediate / afterward / reversibility), an optional inline required-reason field,\n * and an optional exact-phrase typed-confirmation gate. Composes the ui-core AlertDialog primitives\n * (Radix) — it never re-implements them. Supports BOTH a `trigger` (uncontrolled) and `open` /\n * `onOpenChange` (controlled).\n *\n * Pending model: the confirm button is a plain `Button` (NOT `AlertDialogAction`), so Radix does not\n * auto-close on click; open state is managed here so the dialog stays open with a visible spinner\n * until `onConfirm` resolves.\n */\nexport function ConsequenceDialog({\n trigger,\n open,\n onOpenChange,\n title,\n immediateEffect,\n delayedEffect,\n reversibility,\n reason,\n typedConfirmation,\n confirmLabel,\n cancelLabel = 'Cancel',\n destructive = false,\n onConfirm,\n}: ConsequenceDialogProps) {\n const isControlled = open !== undefined;\n const [internalOpen, setInternalOpen] = useState(false);\n const [typedValue, setTypedValue] = useState('');\n const [pending, setPending] = useState(false);\n\n const actualOpen = isControlled ? open : internalOpen;\n\n const reasonId = useId();\n const typedId = useId();\n\n const setOpenState = useCallback(\n (next: boolean) => {\n if (isControlled) onOpenChange?.(next);\n else setInternalOpen(next);\n },\n [isControlled, onOpenChange],\n );\n\n // Never allow an open-state change (Escape, cancel, controlled parent) to fight the pending action.\n const handleOpenChange = useCallback(\n (next: boolean) => {\n if (pending) return;\n setTypedValue(''); // reset the typed gate on every open/close\n setOpenState(next);\n },\n [pending, setOpenState],\n );\n\n const handleConfirm = useCallback(async () => {\n if (pending) return;\n setPending(true);\n try {\n await onConfirm();\n setTypedValue('');\n setOpenState(false);\n } catch {\n // Keep the dialog open — the caller's onConfirm surfaced the error (toast / step-up). The\n // operator can retry or cancel. Never rethrow, never monitor here.\n } finally {\n setPending(false);\n }\n }, [pending, onConfirm, setOpenState]);\n\n const reasonTrimmedLength = reason ? reason.value.trim().length : 0;\n const reasonProvided = reasonTrimmedLength > 0;\n const reasonOk = !reason\n ? true\n : reason.required && !reasonProvided\n ? false\n : !reasonProvided\n ? true // optional + empty is fine\n : (reason.minLength === undefined || reasonTrimmedLength >= reason.minLength) &&\n (reason.maxLength === undefined || reason.value.length <= reason.maxLength);\n\n const typedOk = !typedConfirmation ? true : typedValue === typedConfirmation.phrase;\n const confirmDisabled = pending || !reasonOk || !typedOk;\n\n return (\n <AlertDialog open={actualOpen} onOpenChange={handleOpenChange}>\n {trigger ? <AlertDialogTrigger asChild>{trigger}</AlertDialogTrigger> : null}\n <AlertDialogContent\n onEscapeKeyDown={(e) => {\n if (pending) e.preventDefault();\n }}\n >\n <AlertDialogHeader>\n <AlertDialogTitle>{title}</AlertDialogTitle>\n <AlertDialogDescription asChild>\n <span className=\"stack-3\">\n {immediateEffect ? (\n <ConsequenceRow icon={Zap} label=\"Immediately\">\n {immediateEffect}\n </ConsequenceRow>\n ) : null}\n {delayedEffect ? (\n <ConsequenceRow icon={Clock} label=\"Afterward\">\n {delayedEffect}\n </ConsequenceRow>\n ) : null}\n {reversibility ? (\n <ConsequenceRow icon={RotateCcw} label=\"Reversibility\">\n {reversibility}\n </ConsequenceRow>\n ) : null}\n </span>\n </AlertDialogDescription>\n </AlertDialogHeader>\n\n {reason ? (\n <div className=\"stack-2\">\n <Label htmlFor={reasonId}>{reason.label ?? 'Reason'}</Label>\n <Textarea\n id={reasonId}\n value={reason.value}\n onChange={(e) => reason.onChange(e.target.value)}\n placeholder={reason.placeholder}\n maxLength={reason.maxLength}\n rows={reason.rows ?? 3}\n disabled={pending}\n />\n </div>\n ) : null}\n\n {typedConfirmation ? (\n <div className=\"stack-2\">\n <Label htmlFor={typedId}>\n {typedConfirmation.label ?? (\n <>\n Type <span className=\"font-mono\">{typedConfirmation.phrase}</span> to confirm\n </>\n )}\n </Label>\n <Input\n id={typedId}\n value={typedValue}\n onChange={(e) => setTypedValue(e.target.value)}\n placeholder={typedConfirmation.phrase}\n disabled={pending}\n autoComplete=\"off\"\n />\n </div>\n ) : null}\n\n <AlertDialogFooter>\n <Button\n variant=\"outline\"\n onClick={() => handleOpenChange(false)}\n disabled={pending}\n >\n {cancelLabel}\n </Button>\n <Button\n variant={destructive ? 'destructive' : 'default'}\n onClick={handleConfirm}\n disabled={confirmDisabled}\n aria-busy={pending}\n >\n {pending ? <Loader2 className=\"mr-2 spinner-xs\" aria-hidden=\"true\" /> : null}\n {confirmLabel}\n </Button>\n </AlertDialogFooter>\n </AlertDialogContent>\n </AlertDialog>\n );\n}\n"]}
|
|
1
|
+
{"version":3,"file":"consequence-dialog.js","sourceRoot":"","sources":["../../../src/react/components/consequence-dialog.tsx"],"names":[],"mappings":"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","sourcesContent":["'use client';\n\nimport { useCallback, useId, useState } from 'react';\nimport {\n AlertDialog,\n AlertDialogTrigger,\n AlertDialogContent,\n AlertDialogHeader,\n AlertDialogFooter,\n AlertDialogTitle,\n AlertDialogDescription,\n} from './alert-dialog.js';\nimport { Button } from './button.js';\nimport { Textarea } from './textarea.js';\nimport { Input } from './input.js';\nimport { Label } from './label.js';\nimport { Zap, Clock, RotateCcw } from 'lucide-react';\n\n/**\n * The inline required-reason config. The caller OWNS the reason value/onChange (draft state lives at\n * the call site). Any `minLength`/`maxLength` the caller supplies MUST come from a caller-owned\n * constant — this component declares no business numbers and no copy; it only derives the gate + the\n * Textarea `maxLength` from the values it is handed.\n */\nexport interface ConsequenceDialogReason {\n required?: boolean;\n minLength?: number;\n maxLength?: number;\n placeholder?: string;\n label?: React.ReactNode;\n value: string;\n onChange: (value: string) => void;\n rows?: number;\n}\n\n/**\n * The exact-match typed-confirmation gate. `phrase` is business copy owned by the CALLER; the\n * component only compares the typed value against it.\n */\nexport interface ConsequenceDialogTypedConfirmation {\n phrase: string;\n label?: React.ReactNode;\n}\n\nexport interface ConsequenceDialogProps {\n /** Uncontrolled open model: the element that opens the dialog (rendered `asChild`). */\n trigger?: React.ReactNode;\n /** Controlled open model. Provide with `onOpenChange`. */\n open?: boolean;\n onOpenChange?: (open: boolean) => void;\n\n /** Heading — a string or node so callers can interpolate amounts / names. */\n title: React.ReactNode;\n\n /** SLOT 1 — what happens right now (e.g. \"initiates a Stripe refund of $X now\"). */\n immediateEffect?: React.ReactNode;\n /** SLOT 2 — what settles later, out of band (webhook / worker / async confirmation). */\n delayedEffect?: React.ReactNode;\n /** SLOT 3 — whether/how this can be undone or superseded. */\n reversibility?: React.ReactNode;\n\n /** Inline required-reason field. Optional. */\n reason?: ConsequenceDialogReason;\n /** Exact-phrase typed-confirmation gate. Optional. */\n typedConfirmation?: ConsequenceDialogTypedConfirmation;\n\n confirmLabel: React.ReactNode;\n cancelLabel?: React.ReactNode;\n /** `true` styles the confirm button with `bg-destructive`. */\n destructive?: boolean;\n\n /**\n * Runs on confirm. May be async — while it is pending the dialog STAYS OPEN, both buttons disable,\n * and the confirm button shows its pending spinner. Closes on resolve. If it rejects the dialog\n * stays open so the caller's own surface (error toast, TOTP step-up) can show and the operator can\n * retry or cancel. This component takes no Sentry/monitoring dependency — every error path\n * belongs to the caller's `onConfirm`.\n */\n onConfirm: () => void | Promise<void>;\n}\n\nconst SLOT_ICON_CLASS = 'icon-xs mt-0.5 shrink-0 text-muted-foreground';\n\nfunction ConsequenceRow({\n icon: Icon,\n label,\n children,\n}: {\n icon: typeof Zap;\n label: string;\n children: React.ReactNode;\n}) {\n return (\n <span className=\"flex items-start gap-2\">\n <Icon className={SLOT_ICON_CLASS} aria-hidden=\"true\" />\n <span className=\"stack-1 min-w-0\">\n <span className=\"text-caption font-medium uppercase tracking-wide text-muted-foreground\">\n {label}\n </span>\n <span className=\"text-small text-foreground\">{children}</span>\n </span>\n </span>\n );\n}\n\n/**\n * A reusable confirm-before-a-consequential-action dialog. Three optional, consistently-styled\n * consequence slots (immediate / afterward / reversibility), an optional inline required-reason field,\n * and an optional exact-phrase typed-confirmation gate. Composes the ui-core AlertDialog primitives\n * (Radix) — it never re-implements them. Supports BOTH a `trigger` (uncontrolled) and `open` /\n * `onOpenChange` (controlled).\n *\n * Pending model: the confirm button is a plain `Button` (NOT `AlertDialogAction`), so Radix does not\n * auto-close on click; open state is managed here so the dialog stays open with a visible spinner\n * until `onConfirm` resolves.\n */\nexport function ConsequenceDialog({\n trigger,\n open,\n onOpenChange,\n title,\n immediateEffect,\n delayedEffect,\n reversibility,\n reason,\n typedConfirmation,\n confirmLabel,\n cancelLabel = 'Cancel',\n destructive = false,\n onConfirm,\n}: ConsequenceDialogProps) {\n const isControlled = open !== undefined;\n const [internalOpen, setInternalOpen] = useState(false);\n const [typedValue, setTypedValue] = useState('');\n const [pending, setPending] = useState(false);\n\n const actualOpen = isControlled ? open : internalOpen;\n\n const reasonId = useId();\n const typedId = useId();\n\n const setOpenState = useCallback(\n (next: boolean) => {\n if (isControlled) onOpenChange?.(next);\n else setInternalOpen(next);\n },\n [isControlled, onOpenChange],\n );\n\n // Never allow an open-state change (Escape, cancel, controlled parent) to fight the pending action.\n const handleOpenChange = useCallback(\n (next: boolean) => {\n if (pending) return;\n setTypedValue(''); // reset the typed gate on every open/close\n setOpenState(next);\n },\n [pending, setOpenState],\n );\n\n const handleConfirm = useCallback(async () => {\n if (pending) return;\n setPending(true);\n try {\n await onConfirm();\n setTypedValue('');\n setOpenState(false);\n } catch {\n // Keep the dialog open — the caller's onConfirm surfaced the error (toast / step-up). The\n // operator can retry or cancel. Never rethrow, never monitor here.\n } finally {\n setPending(false);\n }\n }, [pending, onConfirm, setOpenState]);\n\n const reasonTrimmedLength = reason ? reason.value.trim().length : 0;\n const reasonProvided = reasonTrimmedLength > 0;\n const reasonOk = !reason\n ? true\n : reason.required && !reasonProvided\n ? false\n : !reasonProvided\n ? true // optional + empty is fine\n : (reason.minLength === undefined || reasonTrimmedLength >= reason.minLength) &&\n (reason.maxLength === undefined || reason.value.length <= reason.maxLength);\n\n const typedOk = !typedConfirmation ? true : typedValue === typedConfirmation.phrase;\n\n return (\n <AlertDialog open={actualOpen} onOpenChange={handleOpenChange}>\n {trigger ? <AlertDialogTrigger asChild>{trigger}</AlertDialogTrigger> : null}\n <AlertDialogContent\n onEscapeKeyDown={(e) => {\n if (pending) e.preventDefault();\n }}\n >\n <AlertDialogHeader>\n <AlertDialogTitle>{title}</AlertDialogTitle>\n <AlertDialogDescription asChild>\n <span className=\"stack-3\">\n {immediateEffect ? (\n <ConsequenceRow icon={Zap} label=\"Immediately\">\n {immediateEffect}\n </ConsequenceRow>\n ) : null}\n {delayedEffect ? (\n <ConsequenceRow icon={Clock} label=\"Afterward\">\n {delayedEffect}\n </ConsequenceRow>\n ) : null}\n {reversibility ? (\n <ConsequenceRow icon={RotateCcw} label=\"Reversibility\">\n {reversibility}\n </ConsequenceRow>\n ) : null}\n </span>\n </AlertDialogDescription>\n </AlertDialogHeader>\n\n {reason ? (\n <div className=\"stack-2\">\n <Label htmlFor={reasonId}>{reason.label ?? 'Reason'}</Label>\n <Textarea\n id={reasonId}\n value={reason.value}\n onChange={(e) => reason.onChange(e.target.value)}\n placeholder={reason.placeholder}\n maxLength={reason.maxLength}\n rows={reason.rows ?? 3}\n disabled={pending}\n />\n </div>\n ) : null}\n\n {typedConfirmation ? (\n <div className=\"stack-2\">\n <Label htmlFor={typedId}>\n {typedConfirmation.label ?? (\n <>\n Type <span className=\"font-mono\">{typedConfirmation.phrase}</span> to confirm\n </>\n )}\n </Label>\n <Input\n id={typedId}\n value={typedValue}\n onChange={(e) => setTypedValue(e.target.value)}\n placeholder={typedConfirmation.phrase}\n disabled={pending}\n autoComplete=\"off\"\n />\n </div>\n ) : null}\n\n <AlertDialogFooter>\n <Button\n variant=\"outline\"\n onClick={() => handleOpenChange(false)}\n disabled={pending}\n >\n {cancelLabel}\n </Button>\n <Button\n variant={destructive ? 'destructive' : 'default'}\n onClick={handleConfirm}\n disabled={!reasonOk || !typedOk}\n pending={pending}\n >\n {confirmLabel}\n </Button>\n </AlertDialogFooter>\n </AlertDialogContent>\n </AlertDialog>\n );\n}\n"]}
|
|
@@ -13,6 +13,14 @@ declare const DropdownMenuSubContent: React.ForwardRefExoticComponent<Omit<Dropd
|
|
|
13
13
|
declare const DropdownMenuContent: React.ForwardRefExoticComponent<Omit<DropdownMenuPrimitive.DropdownMenuContentProps & React.RefAttributes<HTMLDivElement>, "ref"> & React.RefAttributes<HTMLDivElement>>;
|
|
14
14
|
declare const DropdownMenuItem: React.ForwardRefExoticComponent<Omit<DropdownMenuPrimitive.DropdownMenuItemProps & React.RefAttributes<HTMLDivElement>, "ref"> & {
|
|
15
15
|
inset?: boolean;
|
|
16
|
+
/**
|
|
17
|
+
* The action this item started is in flight: the item disables itself, sets
|
|
18
|
+
* `aria-busy`, and shows the spinner in place of its `icon`. Keeping the menu open
|
|
19
|
+
* meanwhile is the caller's `onSelect` (`event.preventDefault()`, close on settle).
|
|
20
|
+
*/
|
|
21
|
+
pending?: boolean;
|
|
22
|
+
/** Leading icon. While `pending`, the spinner takes its place. */
|
|
23
|
+
icon?: React.ReactNode;
|
|
16
24
|
} & React.RefAttributes<HTMLDivElement>>;
|
|
17
25
|
declare const DropdownMenuCheckboxItem: React.ForwardRefExoticComponent<Omit<DropdownMenuPrimitive.DropdownMenuCheckboxItemProps & React.RefAttributes<HTMLDivElement>, "ref"> & React.RefAttributes<HTMLDivElement>>;
|
|
18
26
|
declare const DropdownMenuRadioItem: React.ForwardRefExoticComponent<Omit<DropdownMenuPrimitive.DropdownMenuRadioItemProps & React.RefAttributes<HTMLDivElement>, "ref"> & React.RefAttributes<HTMLDivElement>>;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"dropdown-menu.d.ts","sourceRoot":"","sources":["../../../src/react/components/dropdown-menu.tsx"],"names":[],"mappings":"AAEA,OAAO,KAAK,KAAK,MAAM,OAAO,CAAA;AAC9B,OAAO,KAAK,qBAAqB,MAAM,+BAA+B,CAAA;
|
|
1
|
+
{"version":3,"file":"dropdown-menu.d.ts","sourceRoot":"","sources":["../../../src/react/components/dropdown-menu.tsx"],"names":[],"mappings":"AAEA,OAAO,KAAK,KAAK,MAAM,OAAO,CAAA;AAC9B,OAAO,KAAK,qBAAqB,MAAM,+BAA+B,CAAA;AAMtE,QAAA,MAAM,YAAY,mDAA6B,CAAA;AAC/C,QAAA,MAAM,mBAAmB,0HAAgC,CAAA;AACzD,QAAA,MAAM,iBAAiB,qHAA8B,CAAA;AACrD,QAAA,MAAM,kBAAkB,yDAA+B,CAAA;AACvD,QAAA,MAAM,eAAe,sDAA4B,CAAA;AACjD,QAAA,MAAM,sBAAsB,0HAAmC,CAAA;AAE/D,QAAA,MAAM,sBAAsB;YAGhB,OAAO;wCAejB,CAAA;AAGF,QAAA,MAAM,sBAAsB,6KAY1B,CAAA;AAGF,QAAA,MAAM,mBAAmB,0KAevB,CAAA;AAGF,QAAA,MAAM,gBAAgB;YAGV,OAAO;IACf;;;;OAIG;cACO,OAAO;IACjB,kEAAkE;WAC3D,KAAK,CAAC,SAAS;wCAiBxB,CAAA;AAGF,QAAA,MAAM,wBAAwB,+KAoB5B,CAAA;AAGF,QAAA,MAAM,qBAAqB,4KAmBzB,CAAA;AAGF,QAAA,MAAM,iBAAiB;YAGX,OAAO;wCAQjB,CAAA;AAGF,QAAA,MAAM,qBAAqB,4KASzB,CAAA;AAGF,QAAA,MAAM,oBAAoB;8BAGvB,KAAK,CAAC,cAAc,CAAC,eAAe,CAAC;;CAEvC,CAAA;AAGD,OAAO,EACL,YAAY,EACZ,mBAAmB,EACnB,mBAAmB,EACnB,gBAAgB,EAChB,wBAAwB,EACxB,qBAAqB,EACrB,iBAAiB,EACjB,qBAAqB,EACrB,oBAAoB,EACpB,iBAAiB,EACjB,kBAAkB,EAClB,eAAe,EACf,sBAAsB,EACtB,sBAAsB,EACtB,sBAAsB,GACvB,CAAA"}
|
|
@@ -4,6 +4,7 @@ import * as React from "react";
|
|
|
4
4
|
import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu";
|
|
5
5
|
import { Check, ChevronRight, Circle } from "lucide-react";
|
|
6
6
|
import { cn } from "../../lib/utils.js";
|
|
7
|
+
import { Spinner } from "./spinner.js";
|
|
7
8
|
const DropdownMenu = DropdownMenuPrimitive.Root;
|
|
8
9
|
const DropdownMenuTrigger = DropdownMenuPrimitive.Trigger;
|
|
9
10
|
const DropdownMenuGroup = DropdownMenuPrimitive.Group;
|
|
@@ -16,7 +17,7 @@ const DropdownMenuSubContent = React.forwardRef(({ className, ...props }, ref) =
|
|
|
16
17
|
DropdownMenuSubContent.displayName = DropdownMenuPrimitive.SubContent.displayName;
|
|
17
18
|
const DropdownMenuContent = React.forwardRef(({ className, sideOffset = 4, ...props }, ref) => (_jsx(DropdownMenuPrimitive.Portal, { children: _jsx(DropdownMenuPrimitive.Content, { ref: ref, sideOffset: sideOffset, className: cn("z-50 min-w-[8rem] overflow-hidden rounded-md border !bg-card p-1 text-popover-foreground shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2", className), ...props }) })));
|
|
18
19
|
DropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName;
|
|
19
|
-
const DropdownMenuItem = React.forwardRef(({ className, inset, ...props }, ref) => (
|
|
20
|
+
const DropdownMenuItem = React.forwardRef(({ className, inset, pending = false, icon, disabled, children, ...props }, ref) => (_jsxs(DropdownMenuPrimitive.Item, { ref: ref, className: cn("relative flex cursor-default select-none items-center gap-2 rounded-xs px-2 py-1.5 text-sm outline-hidden transition-colors data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0 app-dropdown-item", inset && "pl-8", className), ...props, ...(pending ? { "aria-busy": true, "data-pending": "" } : {}), disabled: disabled || pending || undefined, children: [pending ? _jsx(Spinner, { size: "xs" }) : icon, children] })));
|
|
20
21
|
DropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName;
|
|
21
22
|
const DropdownMenuCheckboxItem = React.forwardRef(({ className, children, checked, ...props }, ref) => (_jsxs(DropdownMenuPrimitive.CheckboxItem, { ref: ref, className: cn("relative flex cursor-default select-none items-center rounded-xs py-1.5 pl-8 pr-2 text-sm outline-hidden transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50", className), checked: checked, ...props, children: [_jsx("span", { className: "absolute left-2 flex h-3.5 w-3.5 items-center justify-center", children: _jsx(DropdownMenuPrimitive.ItemIndicator, { children: _jsx(Check, { className: "h-4 w-4" }) }) }), children] })));
|
|
22
23
|
DropdownMenuCheckboxItem.displayName = DropdownMenuPrimitive.CheckboxItem.displayName;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"dropdown-menu.js","sourceRoot":"","sources":["../../../src/react/components/dropdown-menu.tsx"],"names":[],"mappings":"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","sourcesContent":["\"use client\"\n\nimport * as React from \"react\"\nimport * as DropdownMenuPrimitive from \"@radix-ui/react-dropdown-menu\"\nimport { Check, ChevronRight, Circle } from \"lucide-react\"\n\nimport { cn } from \"../../lib/utils.js\"\n\nconst DropdownMenu = DropdownMenuPrimitive.Root\nconst DropdownMenuTrigger = DropdownMenuPrimitive.Trigger\nconst DropdownMenuGroup = DropdownMenuPrimitive.Group\nconst DropdownMenuPortal = DropdownMenuPrimitive.Portal\nconst DropdownMenuSub = DropdownMenuPrimitive.Sub\nconst DropdownMenuRadioGroup = DropdownMenuPrimitive.RadioGroup\n\nconst DropdownMenuSubTrigger = React.forwardRef<\n React.ElementRef<typeof DropdownMenuPrimitive.SubTrigger>,\n React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubTrigger> & {\n inset?: boolean\n }\n>(({ className, inset, children, ...props }, ref) => (\n <DropdownMenuPrimitive.SubTrigger\n ref={ref}\n className={cn(\n \"flex cursor-default gap-2 select-none items-center rounded-xs px-2 py-1.5 text-sm outline-hidden focus:bg-accent data-[state=open]:bg-accent [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0\",\n inset && \"pl-8\",\n className\n )}\n {...props}\n >\n {children}\n <ChevronRight className=\"ml-auto\" />\n </DropdownMenuPrimitive.SubTrigger>\n))\nDropdownMenuSubTrigger.displayName = DropdownMenuPrimitive.SubTrigger.displayName\n\nconst DropdownMenuSubContent = React.forwardRef<\n React.ElementRef<typeof DropdownMenuPrimitive.SubContent>,\n React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubContent>\n>(({ className, ...props }, ref) => (\n <DropdownMenuPrimitive.SubContent\n ref={ref}\n className={cn(\n \"z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-lg data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2\",\n className\n )}\n {...props}\n />\n))\nDropdownMenuSubContent.displayName = DropdownMenuPrimitive.SubContent.displayName\n\nconst DropdownMenuContent = React.forwardRef<\n React.ElementRef<typeof DropdownMenuPrimitive.Content>,\n React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Content>\n>(({ className, sideOffset = 4, ...props }, ref) => (\n <DropdownMenuPrimitive.Portal>\n <DropdownMenuPrimitive.Content\n ref={ref}\n sideOffset={sideOffset}\n className={cn(\n \"z-50 min-w-[8rem] overflow-hidden rounded-md border !bg-card p-1 text-popover-foreground shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2\",\n className\n )}\n {...props}\n />\n </DropdownMenuPrimitive.Portal>\n))\nDropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName\n\nconst DropdownMenuItem = React.forwardRef<\n React.ElementRef<typeof DropdownMenuPrimitive.Item>,\n React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Item> & {\n inset?: boolean\n }\n>(({ className, inset, ...props }, ref) => (\n <DropdownMenuPrimitive.Item\n ref={ref}\n className={cn(\n \"relative flex cursor-default select-none items-center gap-2 rounded-xs px-2 py-1.5 text-sm outline-hidden transition-colors data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0 app-dropdown-item\",\n inset && \"pl-8\",\n className\n )}\n {...props}\n />\n))\nDropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName\n\nconst DropdownMenuCheckboxItem = React.forwardRef<\n React.ElementRef<typeof DropdownMenuPrimitive.CheckboxItem>,\n React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.CheckboxItem>\n>(({ className, children, checked, ...props }, ref) => (\n <DropdownMenuPrimitive.CheckboxItem\n ref={ref}\n className={cn(\n \"relative flex cursor-default select-none items-center rounded-xs py-1.5 pl-8 pr-2 text-sm outline-hidden transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50\",\n className\n )}\n checked={checked}\n {...props}\n >\n <span className=\"absolute left-2 flex h-3.5 w-3.5 items-center justify-center\">\n <DropdownMenuPrimitive.ItemIndicator>\n <Check className=\"h-4 w-4\" />\n </DropdownMenuPrimitive.ItemIndicator>\n </span>\n {children}\n </DropdownMenuPrimitive.CheckboxItem>\n))\nDropdownMenuCheckboxItem.displayName = DropdownMenuPrimitive.CheckboxItem.displayName\n\nconst DropdownMenuRadioItem = React.forwardRef<\n React.ElementRef<typeof DropdownMenuPrimitive.RadioItem>,\n React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.RadioItem>\n>(({ className, children, ...props }, ref) => (\n <DropdownMenuPrimitive.RadioItem\n ref={ref}\n className={cn(\n \"relative flex cursor-default select-none items-center rounded-xs py-1.5 pl-8 pr-2 text-sm outline-hidden transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50\",\n className\n )}\n {...props}\n >\n <span className=\"absolute left-2 flex h-3.5 w-3.5 items-center justify-center\">\n <DropdownMenuPrimitive.ItemIndicator>\n <Circle className=\"h-2 w-2 fill-current\" />\n </DropdownMenuPrimitive.ItemIndicator>\n </span>\n {children}\n </DropdownMenuPrimitive.RadioItem>\n))\nDropdownMenuRadioItem.displayName = DropdownMenuPrimitive.RadioItem.displayName\n\nconst DropdownMenuLabel = React.forwardRef<\n React.ElementRef<typeof DropdownMenuPrimitive.Label>,\n React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Label> & {\n inset?: boolean\n }\n>(({ className, inset, ...props }, ref) => (\n <DropdownMenuPrimitive.Label\n ref={ref}\n className={cn(\"px-2 py-1.5 text-sm font-semibold\", inset && \"pl-8\", className)}\n {...props}\n />\n))\nDropdownMenuLabel.displayName = DropdownMenuPrimitive.Label.displayName\n\nconst DropdownMenuSeparator = React.forwardRef<\n React.ElementRef<typeof DropdownMenuPrimitive.Separator>,\n React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Separator>\n>(({ className, ...props }, ref) => (\n <DropdownMenuPrimitive.Separator\n ref={ref}\n className={cn(\"-mx-1 my-1 h-px bg-muted\", className)}\n {...props}\n />\n))\nDropdownMenuSeparator.displayName = DropdownMenuPrimitive.Separator.displayName\n\nconst DropdownMenuShortcut = ({\n className,\n ...props\n}: React.HTMLAttributes<HTMLSpanElement>) => (\n <span className={cn(\"ml-auto text-xs tracking-widest opacity-60\", className)} {...props} />\n)\nDropdownMenuShortcut.displayName = \"DropdownMenuShortcut\"\n\nexport {\n DropdownMenu,\n DropdownMenuTrigger,\n DropdownMenuContent,\n DropdownMenuItem,\n DropdownMenuCheckboxItem,\n DropdownMenuRadioItem,\n DropdownMenuLabel,\n DropdownMenuSeparator,\n DropdownMenuShortcut,\n DropdownMenuGroup,\n DropdownMenuPortal,\n DropdownMenuSub,\n DropdownMenuSubContent,\n DropdownMenuSubTrigger,\n DropdownMenuRadioGroup,\n}\n"]}
|
|
1
|
+
{"version":3,"file":"dropdown-menu.js","sourceRoot":"","sources":["../../../src/react/components/dropdown-menu.tsx"],"names":[],"mappings":"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","sourcesContent":["\"use client\"\n\nimport * as React from \"react\"\nimport * as DropdownMenuPrimitive from \"@radix-ui/react-dropdown-menu\"\nimport { Check, ChevronRight, Circle } from \"lucide-react\"\n\nimport { cn } from \"../../lib/utils.js\"\nimport { Spinner } from \"./spinner.js\"\n\nconst DropdownMenu = DropdownMenuPrimitive.Root\nconst DropdownMenuTrigger = DropdownMenuPrimitive.Trigger\nconst DropdownMenuGroup = DropdownMenuPrimitive.Group\nconst DropdownMenuPortal = DropdownMenuPrimitive.Portal\nconst DropdownMenuSub = DropdownMenuPrimitive.Sub\nconst DropdownMenuRadioGroup = DropdownMenuPrimitive.RadioGroup\n\nconst DropdownMenuSubTrigger = React.forwardRef<\n React.ElementRef<typeof DropdownMenuPrimitive.SubTrigger>,\n React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubTrigger> & {\n inset?: boolean\n }\n>(({ className, inset, children, ...props }, ref) => (\n <DropdownMenuPrimitive.SubTrigger\n ref={ref}\n className={cn(\n \"flex cursor-default gap-2 select-none items-center rounded-xs px-2 py-1.5 text-sm outline-hidden focus:bg-accent data-[state=open]:bg-accent [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0\",\n inset && \"pl-8\",\n className\n )}\n {...props}\n >\n {children}\n <ChevronRight className=\"ml-auto\" />\n </DropdownMenuPrimitive.SubTrigger>\n))\nDropdownMenuSubTrigger.displayName = DropdownMenuPrimitive.SubTrigger.displayName\n\nconst DropdownMenuSubContent = React.forwardRef<\n React.ElementRef<typeof DropdownMenuPrimitive.SubContent>,\n React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubContent>\n>(({ className, ...props }, ref) => (\n <DropdownMenuPrimitive.SubContent\n ref={ref}\n className={cn(\n \"z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-lg data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2\",\n className\n )}\n {...props}\n />\n))\nDropdownMenuSubContent.displayName = DropdownMenuPrimitive.SubContent.displayName\n\nconst DropdownMenuContent = React.forwardRef<\n React.ElementRef<typeof DropdownMenuPrimitive.Content>,\n React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Content>\n>(({ className, sideOffset = 4, ...props }, ref) => (\n <DropdownMenuPrimitive.Portal>\n <DropdownMenuPrimitive.Content\n ref={ref}\n sideOffset={sideOffset}\n className={cn(\n \"z-50 min-w-[8rem] overflow-hidden rounded-md border !bg-card p-1 text-popover-foreground shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2\",\n className\n )}\n {...props}\n />\n </DropdownMenuPrimitive.Portal>\n))\nDropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName\n\nconst DropdownMenuItem = React.forwardRef<\n React.ElementRef<typeof DropdownMenuPrimitive.Item>,\n React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Item> & {\n inset?: boolean\n /**\n * The action this item started is in flight: the item disables itself, sets\n * `aria-busy`, and shows the spinner in place of its `icon`. Keeping the menu open\n * meanwhile is the caller's `onSelect` (`event.preventDefault()`, close on settle).\n */\n pending?: boolean\n /** Leading icon. While `pending`, the spinner takes its place. */\n icon?: React.ReactNode\n }\n>(({ className, inset, pending = false, icon, disabled, children, ...props }, ref) => (\n <DropdownMenuPrimitive.Item\n ref={ref}\n className={cn(\n \"relative flex cursor-default select-none items-center gap-2 rounded-xs px-2 py-1.5 text-sm outline-hidden transition-colors data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0 app-dropdown-item\",\n inset && \"pl-8\",\n className\n )}\n {...props}\n {...(pending ? { \"aria-busy\": true, \"data-pending\": \"\" } : {})}\n disabled={disabled || pending || undefined}\n >\n {pending ? <Spinner size=\"xs\" /> : icon}\n {children}\n </DropdownMenuPrimitive.Item>\n))\nDropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName\n\nconst DropdownMenuCheckboxItem = React.forwardRef<\n React.ElementRef<typeof DropdownMenuPrimitive.CheckboxItem>,\n React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.CheckboxItem>\n>(({ className, children, checked, ...props }, ref) => (\n <DropdownMenuPrimitive.CheckboxItem\n ref={ref}\n className={cn(\n \"relative flex cursor-default select-none items-center rounded-xs py-1.5 pl-8 pr-2 text-sm outline-hidden transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50\",\n className\n )}\n checked={checked}\n {...props}\n >\n <span className=\"absolute left-2 flex h-3.5 w-3.5 items-center justify-center\">\n <DropdownMenuPrimitive.ItemIndicator>\n <Check className=\"h-4 w-4\" />\n </DropdownMenuPrimitive.ItemIndicator>\n </span>\n {children}\n </DropdownMenuPrimitive.CheckboxItem>\n))\nDropdownMenuCheckboxItem.displayName = DropdownMenuPrimitive.CheckboxItem.displayName\n\nconst DropdownMenuRadioItem = React.forwardRef<\n React.ElementRef<typeof DropdownMenuPrimitive.RadioItem>,\n React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.RadioItem>\n>(({ className, children, ...props }, ref) => (\n <DropdownMenuPrimitive.RadioItem\n ref={ref}\n className={cn(\n \"relative flex cursor-default select-none items-center rounded-xs py-1.5 pl-8 pr-2 text-sm outline-hidden transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50\",\n className\n )}\n {...props}\n >\n <span className=\"absolute left-2 flex h-3.5 w-3.5 items-center justify-center\">\n <DropdownMenuPrimitive.ItemIndicator>\n <Circle className=\"h-2 w-2 fill-current\" />\n </DropdownMenuPrimitive.ItemIndicator>\n </span>\n {children}\n </DropdownMenuPrimitive.RadioItem>\n))\nDropdownMenuRadioItem.displayName = DropdownMenuPrimitive.RadioItem.displayName\n\nconst DropdownMenuLabel = React.forwardRef<\n React.ElementRef<typeof DropdownMenuPrimitive.Label>,\n React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Label> & {\n inset?: boolean\n }\n>(({ className, inset, ...props }, ref) => (\n <DropdownMenuPrimitive.Label\n ref={ref}\n className={cn(\"px-2 py-1.5 text-sm font-semibold\", inset && \"pl-8\", className)}\n {...props}\n />\n))\nDropdownMenuLabel.displayName = DropdownMenuPrimitive.Label.displayName\n\nconst DropdownMenuSeparator = React.forwardRef<\n React.ElementRef<typeof DropdownMenuPrimitive.Separator>,\n React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Separator>\n>(({ className, ...props }, ref) => (\n <DropdownMenuPrimitive.Separator\n ref={ref}\n className={cn(\"-mx-1 my-1 h-px bg-muted\", className)}\n {...props}\n />\n))\nDropdownMenuSeparator.displayName = DropdownMenuPrimitive.Separator.displayName\n\nconst DropdownMenuShortcut = ({\n className,\n ...props\n}: React.HTMLAttributes<HTMLSpanElement>) => (\n <span className={cn(\"ml-auto text-xs tracking-widest opacity-60\", className)} {...props} />\n)\nDropdownMenuShortcut.displayName = \"DropdownMenuShortcut\"\n\nexport {\n DropdownMenu,\n DropdownMenuTrigger,\n DropdownMenuContent,\n DropdownMenuItem,\n DropdownMenuCheckboxItem,\n DropdownMenuRadioItem,\n DropdownMenuLabel,\n DropdownMenuSeparator,\n DropdownMenuShortcut,\n DropdownMenuGroup,\n DropdownMenuPortal,\n DropdownMenuSub,\n DropdownMenuSubContent,\n DropdownMenuSubTrigger,\n DropdownMenuRadioGroup,\n}\n"]}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"list-pagination.d.ts","sourceRoot":"","sources":["../../../src/react/components/list-pagination.tsx"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"list-pagination.d.ts","sourceRoot":"","sources":["../../../src/react/components/list-pagination.tsx"],"names":[],"mappings":"AAMA,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,4BAA4B,CAAC;AAEtE,MAAM,WAAW,mBAAmB;IAClC;;;;OAIG;IACH,UAAU,EAAE,mBAAmB,CAAC;IAChC;;;;OAIG;IACH,IAAI,CAAC,EAAE,OAAO,CAAC;IACf,uEAAuE;IACvE,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAgB,cAAc,CAAC,EAAE,UAAU,EAAE,IAAY,EAAE,SAAS,EAAE,EAAE,mBAAmB,sCA0D1F"}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { useState } from "react";
|
|
3
4
|
import { ChevronLeft, ChevronRight } from "lucide-react";
|
|
4
5
|
import { Button } from "./button.js";
|
|
5
6
|
import { cn } from "../../lib/utils.js";
|
|
@@ -22,6 +23,17 @@ import { cn } from "../../lib/utils.js";
|
|
|
22
23
|
*/
|
|
23
24
|
export function ListPagination({ pagination, busy = false, className }) {
|
|
24
25
|
const { currentPage, totalPages, canPreviousPage, canNextPage, goToPreviousPage, goToNextPage } = pagination;
|
|
26
|
+
// The control that started the in-flight page shows the spinner. Cleared when `busy`
|
|
27
|
+
// ends — a render-phase adjustment, so there is no effect and no stale frame.
|
|
28
|
+
const [clicked, setClicked] = useState(null);
|
|
29
|
+
const [wasBusy, setWasBusy] = useState(busy);
|
|
30
|
+
if (busy !== wasBusy) {
|
|
31
|
+
setWasBusy(busy);
|
|
32
|
+
if (!busy)
|
|
33
|
+
setClicked(null);
|
|
34
|
+
}
|
|
35
|
+
const previousPending = busy && clicked === "previous";
|
|
36
|
+
const nextPending = busy && clicked === "next";
|
|
25
37
|
// One visibility rule for both flavors. For a known total this is exactly
|
|
26
38
|
// `totalPages > 1`: page state is clamped into `1..totalPages`, so a
|
|
27
39
|
// single-page list has no reachable neighbour in either direction, and a
|
|
@@ -30,6 +42,12 @@ export function ListPagination({ pagination, busy = false, className }) {
|
|
|
30
42
|
// controls grey out in place instead of vanishing mid-fetch.
|
|
31
43
|
if (!canPreviousPage && !canNextPage)
|
|
32
44
|
return null;
|
|
33
|
-
return (_jsxs("div", { className: cn("flex w-full items-center justify-between gap-4 mt-4", className), children: [
|
|
45
|
+
return (_jsxs("div", { className: cn("flex w-full items-center justify-between gap-4 mt-4", className), children: [_jsx(Button, { variant: "outline", size: "sm", onClick: () => {
|
|
46
|
+
setClicked("previous");
|
|
47
|
+
goToPreviousPage();
|
|
48
|
+
}, disabled: busy || !canPreviousPage, pending: previousPending, icon: _jsx(ChevronLeft, { className: "icon-xs" }), className: "h-11 min-w-11", children: "Previous" }), _jsx("span", { className: "text-counter", role: "status", children: totalPages === undefined ? `Page ${currentPage}` : `${currentPage} of ${totalPages}` }), _jsxs(Button, { variant: "outline", size: "sm", onClick: () => {
|
|
49
|
+
setClicked("next");
|
|
50
|
+
goToNextPage();
|
|
51
|
+
}, disabled: busy || !canNextPage, pending: nextPending, className: "h-11 min-w-11", children: ["Next", nextPending ? null : _jsx(ChevronRight, { className: "icon-xs" })] })] }));
|
|
34
52
|
}
|
|
35
53
|
//# sourceMappingURL=list-pagination.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"list-pagination.js","sourceRoot":"","sources":["../../../src/react/components/list-pagination.tsx"],"names":[],"mappings":"AAAA,YAAY,CAAC;;AAEb,OAAO,EAAE,WAAW,EAAE,YAAY,EAAE,MAAM,cAAc,CAAC;AACzD,OAAO,EAAE,MAAM,EAAE,MAAM,aAAa,CAAC;AACrC,OAAO,EAAE,EAAE,EAAE,MAAM,oBAAoB,CAAC;AAoBxC;;;;;;;;;;;;;;;;GAgBG;AACH,MAAM,UAAU,cAAc,CAAC,EAAE,UAAU,EAAE,IAAI,GAAG,KAAK,EAAE,SAAS,EAAuB;IACzF,MAAM,EAAE,WAAW,EAAE,UAAU,EAAE,eAAe,EAAE,WAAW,EAAE,gBAAgB,EAAE,YAAY,EAAE,GAC7F,UAAU,CAAC;IAEb,0EAA0E;IAC1E,qEAAqE;IACrE,yEAAyE;IACzE,0EAA0E;IAC1E,yEAAyE;IACzE,6DAA6D;IAC7D,IAAI,CAAC,eAAe,IAAI,CAAC,WAAW;QAAE,OAAO,IAAI,CAAC;IAElD,OAAO,CACL,eAAK,SAAS,EAAE,EAAE,CAAC,qDAAqD,EAAE,SAAS,CAAC,aAClF,
|
|
1
|
+
{"version":3,"file":"list-pagination.js","sourceRoot":"","sources":["../../../src/react/components/list-pagination.tsx"],"names":[],"mappings":"AAAA,YAAY,CAAC;;AAEb,OAAO,EAAE,QAAQ,EAAE,MAAM,OAAO,CAAC;AACjC,OAAO,EAAE,WAAW,EAAE,YAAY,EAAE,MAAM,cAAc,CAAC;AACzD,OAAO,EAAE,MAAM,EAAE,MAAM,aAAa,CAAC;AACrC,OAAO,EAAE,EAAE,EAAE,MAAM,oBAAoB,CAAC;AAoBxC;;;;;;;;;;;;;;;;GAgBG;AACH,MAAM,UAAU,cAAc,CAAC,EAAE,UAAU,EAAE,IAAI,GAAG,KAAK,EAAE,SAAS,EAAuB;IACzF,MAAM,EAAE,WAAW,EAAE,UAAU,EAAE,eAAe,EAAE,WAAW,EAAE,gBAAgB,EAAE,YAAY,EAAE,GAC7F,UAAU,CAAC;IAEb,qFAAqF;IACrF,8EAA8E;IAC9E,MAAM,CAAC,OAAO,EAAE,UAAU,CAAC,GAAG,QAAQ,CAA6B,IAAI,CAAC,CAAC;IACzE,MAAM,CAAC,OAAO,EAAE,UAAU,CAAC,GAAG,QAAQ,CAAC,IAAI,CAAC,CAAC;IAC7C,IAAI,IAAI,KAAK,OAAO,EAAE,CAAC;QACrB,UAAU,CAAC,IAAI,CAAC,CAAC;QACjB,IAAI,CAAC,IAAI;YAAE,UAAU,CAAC,IAAI,CAAC,CAAC;IAC9B,CAAC;IACD,MAAM,eAAe,GAAG,IAAI,IAAI,OAAO,KAAK,UAAU,CAAC;IACvD,MAAM,WAAW,GAAG,IAAI,IAAI,OAAO,KAAK,MAAM,CAAC;IAE/C,0EAA0E;IAC1E,qEAAqE;IACrE,yEAAyE;IACzE,0EAA0E;IAC1E,yEAAyE;IACzE,6DAA6D;IAC7D,IAAI,CAAC,eAAe,IAAI,CAAC,WAAW;QAAE,OAAO,IAAI,CAAC;IAElD,OAAO,CACL,eAAK,SAAS,EAAE,EAAE,CAAC,qDAAqD,EAAE,SAAS,CAAC,aAClF,KAAC,MAAM,IACL,OAAO,EAAC,SAAS,EACjB,IAAI,EAAC,IAAI,EACT,OAAO,EAAE,GAAG,EAAE;oBACZ,UAAU,CAAC,UAAU,CAAC,CAAC;oBACvB,gBAAgB,EAAE,CAAC;gBACrB,CAAC,EACD,QAAQ,EAAE,IAAI,IAAI,CAAC,eAAe,EAClC,OAAO,EAAE,eAAe,EACxB,IAAI,EAAE,KAAC,WAAW,IAAC,SAAS,EAAC,SAAS,GAAG,EACzC,SAAS,EAAC,eAAe,yBAGlB,EACT,eAAM,SAAS,EAAC,cAAc,EAAC,IAAI,EAAC,QAAQ,YACzC,UAAU,KAAK,SAAS,CAAC,CAAC,CAAC,QAAQ,WAAW,EAAE,CAAC,CAAC,CAAC,GAAG,WAAW,OAAO,UAAU,EAAE,GAChF,EACP,MAAC,MAAM,IACL,OAAO,EAAC,SAAS,EACjB,IAAI,EAAC,IAAI,EACT,OAAO,EAAE,GAAG,EAAE;oBACZ,UAAU,CAAC,MAAM,CAAC,CAAC;oBACnB,YAAY,EAAE,CAAC;gBACjB,CAAC,EACD,QAAQ,EAAE,IAAI,IAAI,CAAC,WAAW,EAC9B,OAAO,EAAE,WAAW,EACpB,SAAS,EAAC,eAAe,qBAGxB,WAAW,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,KAAC,YAAY,IAAC,SAAS,EAAC,SAAS,GAAG,IACnD,IACL,CACP,CAAC;AACJ,CAAC","sourcesContent":["\"use client\";\n\nimport { useState } from \"react\";\nimport { ChevronLeft, ChevronRight } from \"lucide-react\";\nimport { Button } from \"./button.js\";\nimport { cn } from \"../../lib/utils.js\";\nimport type { ListPaginationState } from \"../hooks/use-paged-list.js\";\n\nexport interface ListPaginationProps {\n /**\n * Page state from `usePagedList` (known total),\n * `useCursorPage().paginationFor(hasMore)` (unknown total), or any\n * caller-owned state of the same shape.\n */\n pagination: ListPaginationState;\n /**\n * Disables BOTH controls while the next page is in flight, without hiding the\n * row. Server-paged surfaces pass their query's `isFetching`; a slice-paged\n * list has nothing to wait for and omits it.\n */\n busy?: boolean;\n /** Optional extra classes. Merged with the component's own classes. */\n className?: string;\n}\n\n/**\n * The ONE Previous / counter / Next control for paginated lists, in both\n * flavors. Pair it with the hook that owns the page state (`usePagedList` for a\n * client-side slice, `useCursorPage` — or the caller's own query state — for a\n * cursor feed; a cursor feed passes `pager.paginationFor(hasMore)`, which binds\n * the query's answer to these controls); this component owns only the controls,\n * so every paginated list reads and behaves the same (ENG-002, FRONTEND-001).\n *\n * The counter follows the flavor: a known `totalPages` reads \"2 of 5\", an\n * omitted one reads \"Page 2\". Everything else is identical in both flavors —\n * which is exactly why there is one component and not two.\n *\n * It renders NOTHING when neither direction is available, so no call site\n * repeats a `totalPages > 1` / `page > 1 || hasMore` guard. Edge state is real\n * `disabled` on the buttons (announced, and unclickable), and the counter is a\n * `status` live region so a page change is announced to screen readers.\n */\nexport function ListPagination({ pagination, busy = false, className }: ListPaginationProps) {\n const { currentPage, totalPages, canPreviousPage, canNextPage, goToPreviousPage, goToNextPage } =\n pagination;\n\n // The control that started the in-flight page shows the spinner. Cleared when `busy`\n // ends — a render-phase adjustment, so there is no effect and no stale frame.\n const [clicked, setClicked] = useState<\"previous\" | \"next\" | null>(null);\n const [wasBusy, setWasBusy] = useState(busy);\n if (busy !== wasBusy) {\n setWasBusy(busy);\n if (!busy) setClicked(null);\n }\n const previousPending = busy && clicked === \"previous\";\n const nextPending = busy && clicked === \"next\";\n\n // One visibility rule for both flavors. For a known total this is exactly\n // `totalPages > 1`: page state is clamped into `1..totalPages`, so a\n // single-page list has no reachable neighbour in either direction, and a\n // multi-page list always has one. For a cursor feed it is the hand-rolled\n // `page > 1 || hasMore`. `busy` deliberately does not hide the row — the\n // controls grey out in place instead of vanishing mid-fetch.\n if (!canPreviousPage && !canNextPage) return null;\n\n return (\n <div className={cn(\"flex w-full items-center justify-between gap-4 mt-4\", className)}>\n <Button\n variant=\"outline\"\n size=\"sm\"\n onClick={() => {\n setClicked(\"previous\");\n goToPreviousPage();\n }}\n disabled={busy || !canPreviousPage}\n pending={previousPending}\n icon={<ChevronLeft className=\"icon-xs\" />}\n className=\"h-11 min-w-11\"\n >\n Previous\n </Button>\n <span className=\"text-counter\" role=\"status\">\n {totalPages === undefined ? `Page ${currentPage}` : `${currentPage} of ${totalPages}`}\n </span>\n <Button\n variant=\"outline\"\n size=\"sm\"\n onClick={() => {\n setClicked(\"next\");\n goToNextPage();\n }}\n disabled={busy || !canNextPage}\n pending={nextPending}\n className=\"h-11 min-w-11\"\n >\n Next\n {nextPending ? null : <ChevronRight className=\"icon-xs\" />}\n </Button>\n </div>\n );\n}\n"]}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"search-dropdown.d.ts","sourceRoot":"","sources":["../../../src/react/components/search-dropdown.tsx"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"search-dropdown.d.ts","sourceRoot":"","sources":["../../../src/react/components/search-dropdown.tsx"],"names":[],"mappings":"AASA,MAAM,WAAW,mBAAmB,CAAC,CAAC;IACpC,2BAA2B;IAC3B,KAAK,EAAE,MAAM,CAAC;IACd,uCAAuC;IACvC,aAAa,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;IACvC,2BAA2B;IAC3B,OAAO,EAAE,CAAC,EAAE,CAAC;IACb,oBAAoB;IACpB,SAAS,EAAE,OAAO,CAAC;IACnB,oBAAoB;IACpB,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;IACrB,uCAAuC;IACvC,QAAQ,EAAE,CAAC,MAAM,EAAE,CAAC,KAAK,IAAI,CAAC;IAC9B,kGAAkG;IAClG,OAAO,CAAC,EAAE,MAAM,IAAI,CAAC;IACrB,uBAAuB;IACvB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,0BAA0B;IAC1B,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,uBAAuB;IACvB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,qBAAqB;IACrB,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,8CAA8C;IAC9C,IAAI,CAAC,EAAE,KAAK,CAAC,SAAS,CAAC;IACvB,6DAA6D;IAC7D,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,4CAA4C;IAC5C,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,6CAA6C;IAC7C,YAAY,EAAE,CAAC,MAAM,EAAE,CAAC,EAAE,KAAK,EAAE,MAAM,KAAK,KAAK,CAAC,SAAS,CAAC;CAC7D;AAED;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,wBAAgB,cAAc,CAAC,CAAC,EAAE,EAChC,KAAK,EACL,aAAa,EACb,OAAO,EACP,SAAS,EACT,KAAK,EACL,QAAQ,EACR,OAAO,EACP,WAAyB,EACzB,KAAK,EACL,SAAS,EACT,QAAgB,EAChB,IAAqC,EACrC,QAAY,EACZ,YAAiC,EACjC,YAAY,GACb,EAAE,mBAAmB,CAAC,CAAC,CAAC,+BAyJxB"}
|
|
@@ -3,7 +3,8 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
3
3
|
import { useState, useRef, useEffect } from 'react';
|
|
4
4
|
import { Input } from './input.js';
|
|
5
5
|
import { Label } from './label.js';
|
|
6
|
-
import {
|
|
6
|
+
import { X, Search } from 'lucide-react';
|
|
7
|
+
import { Spinner } from './spinner.js';
|
|
7
8
|
import { cn } from "../../lib/utils.js";
|
|
8
9
|
/**
|
|
9
10
|
* Generic search dropdown component with debounced input and keyboard navigation.
|
|
@@ -88,6 +89,6 @@ export function SearchDropdown({ value, onValueChange, results, isLoading, error
|
|
|
88
89
|
inputRef.current?.focus();
|
|
89
90
|
};
|
|
90
91
|
const showDropdown = isOpen && value.length >= minChars;
|
|
91
|
-
return (_jsxs("div", { ref: containerRef, className: cn('relative', className), children: [label && (_jsx(Label, { htmlFor: "search-input", className: "mb-2 block", children: label })), _jsxs("div", { className: "relative", children: [_jsx("div", { className: "absolute left-3 top-1/2 -translate-y-1/2 text-muted-foreground", children: icon }), _jsx(Input, { ref: inputRef, id: "search-input", type: "text", value: value, onChange: (e) => onValueChange(e.target.value), onKeyDown: handleKeyDown, placeholder: placeholder, disabled: disabled, className: "pl-10 pr-10" }), _jsx("div", { className: "absolute right-3 top-1/2 -translate-y-1/2", children: isLoading ? (_jsx(
|
|
92
|
+
return (_jsxs("div", { ref: containerRef, className: cn('relative', className), children: [label && (_jsx(Label, { htmlFor: "search-input", className: "mb-2 block", children: label })), _jsxs("div", { className: "relative", children: [_jsx("div", { className: "absolute left-3 top-1/2 -translate-y-1/2 text-muted-foreground", children: icon }), _jsx(Input, { ref: inputRef, id: "search-input", type: "text", value: value, onChange: (e) => onValueChange(e.target.value), onKeyDown: handleKeyDown, placeholder: placeholder, disabled: disabled, className: "pl-10 pr-10" }), _jsx("div", { className: "absolute right-3 top-1/2 -translate-y-1/2", children: isLoading ? (_jsx(Spinner, { size: "xs", className: "text-muted-foreground" })) : value ? (_jsx("button", { type: "button", onClick: handleClear, className: "text-muted-foreground hover:text-foreground transition-colors", "aria-label": "Clear search", children: _jsx(X, { className: "h-4 w-4" }) })) : null })] }), showDropdown && (_jsx("div", { className: "absolute z-50 w-full mt-1 bg-popover border rounded-md shadow-lg max-h-60 overflow-auto", children: error ? (_jsx("div", { className: "px-3 py-2 text-sm text-destructive", children: error })) : isLoading ? (_jsxs("div", { className: "px-3 py-2 text-sm text-muted-foreground flex items-center gap-2", children: [_jsx(Spinner, { size: "xs" }), "Searching..."] })) : results.length === 0 ? (_jsx("div", { className: "px-3 py-2 text-sm text-muted-foreground", children: emptyMessage })) : (results.map((result, index) => (_jsx("button", { type: "button", onClick: () => handleResultClick(result), onMouseEnter: () => setSelectedIndex(index), className: cn('w-full text-left transition-colors cursor-pointer', 'hover:bg-accent focus:bg-accent focus:outline-hidden', selectedIndex === index && 'bg-accent'), children: renderResult(result, index) }, index)))) })), value.length > 0 && value.length < minChars && (_jsxs("p", { className: "text-xs text-muted-foreground mt-1", children: ["Type at least ", minChars, " characters to search"] }))] }));
|
|
92
93
|
}
|
|
93
94
|
//# sourceMappingURL=search-dropdown.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"search-dropdown.js","sourceRoot":"","sources":["../../../src/react/components/search-dropdown.tsx"],"names":[],"mappings":"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","sourcesContent":["'use client';\n\nimport { useState, useRef, useEffect } from 'react';\nimport { Input } from './input.js';\nimport { Label } from './label.js';\nimport { Loader2, X, Search } from 'lucide-react';\nimport { cn } from \"../../lib/utils.js\";\n\nexport interface SearchDropdownProps<T> {\n /** Current search value */\n value: string;\n /** Called when search value changes */\n onValueChange: (value: string) => void;\n /** Search results array */\n results: T[];\n /** Loading state */\n isLoading: boolean;\n /** Error message */\n error: string | null;\n /** Called when a result is selected */\n onSelect: (result: T) => void;\n /** Called when search is cleared (after the value has been reset). Optional notification hook. */\n onClear?: () => void;\n /** Placeholder text */\n placeholder?: string;\n /** Label for the input */\n label?: string;\n /** Custom className */\n className?: string;\n /** Disabled state */\n disabled?: boolean;\n /** Icon to show in input (default: Search) */\n icon?: React.ReactNode;\n /** Minimum characters before showing results (default: 3) */\n minChars?: number;\n /** Message to show when no results found */\n emptyMessage?: string;\n /** Custom render function for each result */\n renderResult: (result: T, index: number) => React.ReactNode;\n}\n\n/**\n * Generic search dropdown component with debounced input and keyboard navigation.\n * Supports any data type and custom rendering.\n * \n * @example\n * ```tsx\n * <SearchDropdown<User>\n * value={searchValue}\n * onValueChange={setSearchValue}\n * results={users}\n * isLoading={isLoading}\n * error={error}\n * onSelect={(user) => console.log(user)}\n * onClear={() => setSearchValue('')}\n * placeholder=\"Search users...\"\n * renderResult={(user) => (\n * <div>{user.displayName}</div>\n * )}\n * />\n * ```\n */\nexport function SearchDropdown<T>({\n value,\n onValueChange,\n results,\n isLoading,\n error,\n onSelect,\n onClear,\n placeholder = 'Search...',\n label,\n className,\n disabled = false,\n icon = <Search className=\"h-4 w-4\" />,\n minChars = 3,\n emptyMessage = 'No results found',\n renderResult,\n}: SearchDropdownProps<T>) {\n const [isOpen, setIsOpen] = useState(false);\n const [selectedIndex, setSelectedIndex] = useState(-1);\n const containerRef = useRef<HTMLDivElement>(null);\n const inputRef = useRef<HTMLInputElement>(null);\n\n useEffect(() => {\n const handleClickOutside = (event: MouseEvent) => {\n if (containerRef.current && !containerRef.current.contains(event.target as Node)) {\n setIsOpen(false);\n }\n };\n\n document.addEventListener('mousedown', handleClickOutside);\n return () => document.removeEventListener('mousedown', handleClickOutside);\n }, []);\n\n useEffect(() => {\n if (value.length >= minChars && (results.length > 0 || isLoading || error)) {\n setIsOpen(true);\n } else {\n setIsOpen(false);\n }\n }, [value, results, isLoading, error, minChars]);\n\n useEffect(() => {\n setSelectedIndex(-1);\n }, [results]);\n\n const handleKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {\n if (!isOpen || results.length === 0) return;\n\n switch (e.key) {\n case 'ArrowDown':\n e.preventDefault();\n setSelectedIndex((prev) => (prev < results.length - 1 ? prev + 1 : prev));\n break;\n case 'ArrowUp':\n e.preventDefault();\n setSelectedIndex((prev) => (prev > 0 ? prev - 1 : 0));\n break;\n case 'Enter':\n e.preventDefault();\n if (selectedIndex >= 0 && selectedIndex < results.length) {\n onSelect(results[selectedIndex]);\n setIsOpen(false);\n }\n break;\n case 'Escape':\n e.preventDefault();\n setIsOpen(false);\n inputRef.current?.blur();\n break;\n }\n };\n\n const handleResultClick = (result: T) => {\n onSelect(result);\n setIsOpen(false);\n };\n\n const handleClear = () => {\n onValueChange('');\n onClear?.();\n setIsOpen(false);\n inputRef.current?.focus();\n };\n\n const showDropdown = isOpen && value.length >= minChars;\n\n return (\n <div ref={containerRef} className={cn('relative', className)}>\n {label && (\n <Label htmlFor=\"search-input\" className=\"mb-2 block\">\n {label}\n </Label>\n )}\n \n <div className=\"relative\">\n <div className=\"absolute left-3 top-1/2 -translate-y-1/2 text-muted-foreground\">\n {icon}\n </div>\n \n <Input\n ref={inputRef}\n id=\"search-input\"\n type=\"text\"\n value={value}\n onChange={(e) => onValueChange(e.target.value)}\n onKeyDown={handleKeyDown}\n placeholder={placeholder}\n disabled={disabled}\n className=\"pl-10 pr-10\"\n />\n\n <div className=\"absolute right-3 top-1/2 -translate-y-1/2\">\n {isLoading ? (\n <Loader2 className=\"h-4 w-4 animate-spin text-muted-foreground\" />\n ) : value ? (\n <button\n type=\"button\"\n onClick={handleClear}\n className=\"text-muted-foreground hover:text-foreground transition-colors\"\n aria-label=\"Clear search\"\n >\n <X className=\"h-4 w-4\" />\n </button>\n ) : null}\n </div>\n </div>\n\n {showDropdown && (\n <div className=\"absolute z-50 w-full mt-1 bg-popover border rounded-md shadow-lg max-h-60 overflow-auto\">\n {error ? (\n <div className=\"px-3 py-2 text-sm text-destructive\">\n {error}\n </div>\n ) : isLoading ? (\n <div className=\"px-3 py-2 text-sm text-muted-foreground flex items-center gap-2\">\n <Loader2 className=\"h-4 w-4 animate-spin\" />\n Searching...\n </div>\n ) : results.length === 0 ? (\n <div className=\"px-3 py-2 text-sm text-muted-foreground\">\n {emptyMessage}\n </div>\n ) : (\n results.map((result, index) => (\n <button\n key={index}\n type=\"button\"\n onClick={() => handleResultClick(result)}\n onMouseEnter={() => setSelectedIndex(index)}\n className={cn(\n 'w-full text-left transition-colors cursor-pointer',\n 'hover:bg-accent focus:bg-accent focus:outline-hidden',\n selectedIndex === index && 'bg-accent'\n )}\n >\n {renderResult(result, index)}\n </button>\n ))\n )}\n </div>\n )}\n \n {value.length > 0 && value.length < minChars && (\n <p className=\"text-xs text-muted-foreground mt-1\">\n Type at least {minChars} characters to search\n </p>\n )}\n </div>\n );\n}\n"]}
|
|
1
|
+
{"version":3,"file":"search-dropdown.js","sourceRoot":"","sources":["../../../src/react/components/search-dropdown.tsx"],"names":[],"mappings":"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","sourcesContent":["'use client';\n\nimport { useState, useRef, useEffect } from 'react';\nimport { Input } from './input.js';\nimport { Label } from './label.js';\nimport { X, Search } from 'lucide-react';\nimport { Spinner } from './spinner.js';\nimport { cn } from \"../../lib/utils.js\";\n\nexport interface SearchDropdownProps<T> {\n /** Current search value */\n value: string;\n /** Called when search value changes */\n onValueChange: (value: string) => void;\n /** Search results array */\n results: T[];\n /** Loading state */\n isLoading: boolean;\n /** Error message */\n error: string | null;\n /** Called when a result is selected */\n onSelect: (result: T) => void;\n /** Called when search is cleared (after the value has been reset). Optional notification hook. */\n onClear?: () => void;\n /** Placeholder text */\n placeholder?: string;\n /** Label for the input */\n label?: string;\n /** Custom className */\n className?: string;\n /** Disabled state */\n disabled?: boolean;\n /** Icon to show in input (default: Search) */\n icon?: React.ReactNode;\n /** Minimum characters before showing results (default: 3) */\n minChars?: number;\n /** Message to show when no results found */\n emptyMessage?: string;\n /** Custom render function for each result */\n renderResult: (result: T, index: number) => React.ReactNode;\n}\n\n/**\n * Generic search dropdown component with debounced input and keyboard navigation.\n * Supports any data type and custom rendering.\n * \n * @example\n * ```tsx\n * <SearchDropdown<User>\n * value={searchValue}\n * onValueChange={setSearchValue}\n * results={users}\n * isLoading={isLoading}\n * error={error}\n * onSelect={(user) => console.log(user)}\n * onClear={() => setSearchValue('')}\n * placeholder=\"Search users...\"\n * renderResult={(user) => (\n * <div>{user.displayName}</div>\n * )}\n * />\n * ```\n */\nexport function SearchDropdown<T>({\n value,\n onValueChange,\n results,\n isLoading,\n error,\n onSelect,\n onClear,\n placeholder = 'Search...',\n label,\n className,\n disabled = false,\n icon = <Search className=\"h-4 w-4\" />,\n minChars = 3,\n emptyMessage = 'No results found',\n renderResult,\n}: SearchDropdownProps<T>) {\n const [isOpen, setIsOpen] = useState(false);\n const [selectedIndex, setSelectedIndex] = useState(-1);\n const containerRef = useRef<HTMLDivElement>(null);\n const inputRef = useRef<HTMLInputElement>(null);\n\n useEffect(() => {\n const handleClickOutside = (event: MouseEvent) => {\n if (containerRef.current && !containerRef.current.contains(event.target as Node)) {\n setIsOpen(false);\n }\n };\n\n document.addEventListener('mousedown', handleClickOutside);\n return () => document.removeEventListener('mousedown', handleClickOutside);\n }, []);\n\n useEffect(() => {\n if (value.length >= minChars && (results.length > 0 || isLoading || error)) {\n setIsOpen(true);\n } else {\n setIsOpen(false);\n }\n }, [value, results, isLoading, error, minChars]);\n\n useEffect(() => {\n setSelectedIndex(-1);\n }, [results]);\n\n const handleKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {\n if (!isOpen || results.length === 0) return;\n\n switch (e.key) {\n case 'ArrowDown':\n e.preventDefault();\n setSelectedIndex((prev) => (prev < results.length - 1 ? prev + 1 : prev));\n break;\n case 'ArrowUp':\n e.preventDefault();\n setSelectedIndex((prev) => (prev > 0 ? prev - 1 : 0));\n break;\n case 'Enter':\n e.preventDefault();\n if (selectedIndex >= 0 && selectedIndex < results.length) {\n onSelect(results[selectedIndex]);\n setIsOpen(false);\n }\n break;\n case 'Escape':\n e.preventDefault();\n setIsOpen(false);\n inputRef.current?.blur();\n break;\n }\n };\n\n const handleResultClick = (result: T) => {\n onSelect(result);\n setIsOpen(false);\n };\n\n const handleClear = () => {\n onValueChange('');\n onClear?.();\n setIsOpen(false);\n inputRef.current?.focus();\n };\n\n const showDropdown = isOpen && value.length >= minChars;\n\n return (\n <div ref={containerRef} className={cn('relative', className)}>\n {label && (\n <Label htmlFor=\"search-input\" className=\"mb-2 block\">\n {label}\n </Label>\n )}\n \n <div className=\"relative\">\n <div className=\"absolute left-3 top-1/2 -translate-y-1/2 text-muted-foreground\">\n {icon}\n </div>\n \n <Input\n ref={inputRef}\n id=\"search-input\"\n type=\"text\"\n value={value}\n onChange={(e) => onValueChange(e.target.value)}\n onKeyDown={handleKeyDown}\n placeholder={placeholder}\n disabled={disabled}\n className=\"pl-10 pr-10\"\n />\n\n <div className=\"absolute right-3 top-1/2 -translate-y-1/2\">\n {isLoading ? (\n <Spinner size=\"xs\" className=\"text-muted-foreground\" />\n ) : value ? (\n <button\n type=\"button\"\n onClick={handleClear}\n className=\"text-muted-foreground hover:text-foreground transition-colors\"\n aria-label=\"Clear search\"\n >\n <X className=\"h-4 w-4\" />\n </button>\n ) : null}\n </div>\n </div>\n\n {showDropdown && (\n <div className=\"absolute z-50 w-full mt-1 bg-popover border rounded-md shadow-lg max-h-60 overflow-auto\">\n {error ? (\n <div className=\"px-3 py-2 text-sm text-destructive\">\n {error}\n </div>\n ) : isLoading ? (\n <div className=\"px-3 py-2 text-sm text-muted-foreground flex items-center gap-2\">\n <Spinner size=\"xs\" />\n Searching...\n </div>\n ) : results.length === 0 ? (\n <div className=\"px-3 py-2 text-sm text-muted-foreground\">\n {emptyMessage}\n </div>\n ) : (\n results.map((result, index) => (\n <button\n key={index}\n type=\"button\"\n onClick={() => handleResultClick(result)}\n onMouseEnter={() => setSelectedIndex(index)}\n className={cn(\n 'w-full text-left transition-colors cursor-pointer',\n 'hover:bg-accent focus:bg-accent focus:outline-hidden',\n selectedIndex === index && 'bg-accent'\n )}\n >\n {renderResult(result, index)}\n </button>\n ))\n )}\n </div>\n )}\n \n {value.length > 0 && value.length < minChars && (\n <p className=\"text-xs text-muted-foreground mt-1\">\n Type at least {minChars} characters to search\n </p>\n )}\n </div>\n );\n}\n"]}
|
|
@@ -3,7 +3,14 @@ import * as SelectPrimitive from "@radix-ui/react-select";
|
|
|
3
3
|
declare const Select: React.FC<SelectPrimitive.SelectProps>;
|
|
4
4
|
declare const SelectGroup: React.ForwardRefExoticComponent<SelectPrimitive.SelectGroupProps & React.RefAttributes<HTMLDivElement>>;
|
|
5
5
|
declare const SelectValue: React.ForwardRefExoticComponent<SelectPrimitive.SelectValueProps & React.RefAttributes<HTMLSpanElement>>;
|
|
6
|
-
declare const SelectTrigger: React.ForwardRefExoticComponent<Omit<SelectPrimitive.SelectTriggerProps & React.RefAttributes<HTMLButtonElement>, "ref"> &
|
|
6
|
+
declare const SelectTrigger: React.ForwardRefExoticComponent<Omit<SelectPrimitive.SelectTriggerProps & React.RefAttributes<HTMLButtonElement>, "ref"> & {
|
|
7
|
+
/**
|
|
8
|
+
* The change this select started is in flight: the trigger disables itself, sets
|
|
9
|
+
* `aria-busy`, and shows the spinner in place of its chevron. It keeps showing the
|
|
10
|
+
* COMMITTED value — the caller changes `value` only once the write lands.
|
|
11
|
+
*/
|
|
12
|
+
pending?: boolean;
|
|
13
|
+
} & React.RefAttributes<HTMLButtonElement>>;
|
|
7
14
|
declare const SelectScrollUpButton: React.ForwardRefExoticComponent<Omit<SelectPrimitive.SelectScrollUpButtonProps & React.RefAttributes<HTMLDivElement>, "ref"> & React.RefAttributes<HTMLDivElement>>;
|
|
8
15
|
declare const SelectScrollDownButton: React.ForwardRefExoticComponent<Omit<SelectPrimitive.SelectScrollDownButtonProps & React.RefAttributes<HTMLDivElement>, "ref"> & React.RefAttributes<HTMLDivElement>>;
|
|
9
16
|
declare const SelectContent: React.ForwardRefExoticComponent<Omit<SelectPrimitive.SelectContentProps & React.RefAttributes<HTMLDivElement>, "ref"> & React.RefAttributes<HTMLDivElement>>;
|