@ai-matrx/kit 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +132 -0
- package/LICENSE +21 -0
- package/README.md +72 -0
- package/dist/autosave.cjs +92 -0
- package/dist/autosave.cjs.map +1 -0
- package/dist/autosave.d.cts +17 -0
- package/dist/autosave.d.ts +17 -0
- package/dist/autosave.js +72 -0
- package/dist/autosave.js.map +1 -0
- package/dist/clipboard.cjs +152 -0
- package/dist/clipboard.cjs.map +1 -0
- package/dist/clipboard.d.cts +17 -0
- package/dist/clipboard.d.ts +17 -0
- package/dist/clipboard.js +132 -0
- package/dist/clipboard.js.map +1 -0
- package/dist/color-util.cjs +790 -0
- package/dist/color-util.cjs.map +1 -0
- package/dist/color-util.d.cts +235 -0
- package/dist/color-util.d.ts +235 -0
- package/dist/color-util.js +767 -0
- package/dist/color-util.js.map +1 -0
- package/dist/concurrency.cjs +59 -0
- package/dist/concurrency.cjs.map +1 -0
- package/dist/concurrency.d.cts +19 -0
- package/dist/concurrency.d.ts +19 -0
- package/dist/concurrency.js +38 -0
- package/dist/concurrency.js.map +1 -0
- package/dist/confirm.cjs +344 -0
- package/dist/confirm.cjs.map +1 -0
- package/dist/confirm.d.cts +132 -0
- package/dist/confirm.d.ts +132 -0
- package/dist/confirm.js +312 -0
- package/dist/confirm.js.map +1 -0
- package/dist/delimiter-guard.cjs +172 -0
- package/dist/delimiter-guard.cjs.map +1 -0
- package/dist/delimiter-guard.d.cts +119 -0
- package/dist/delimiter-guard.d.ts +119 -0
- package/dist/delimiter-guard.js +151 -0
- package/dist/delimiter-guard.js.map +1 -0
- package/dist/drafts.cjs +223 -0
- package/dist/drafts.cjs.map +1 -0
- package/dist/drafts.d.cts +62 -0
- package/dist/drafts.d.ts +62 -0
- package/dist/drafts.js +201 -0
- package/dist/drafts.js.map +1 -0
- package/dist/idb-store.cjs +178 -0
- package/dist/idb-store.cjs.map +1 -0
- package/dist/idb-store.d.cts +111 -0
- package/dist/idb-store.d.ts +111 -0
- package/dist/idb-store.js +155 -0
- package/dist/idb-store.js.map +1 -0
- package/dist/idle-scheduler.cjs +268 -0
- package/dist/idle-scheduler.cjs.map +1 -0
- package/dist/idle-scheduler.d.cts +185 -0
- package/dist/idle-scheduler.d.ts +185 -0
- package/dist/idle-scheduler.js +246 -0
- package/dist/idle-scheduler.js.map +1 -0
- package/dist/index.cjs +3113 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +897 -0
- package/dist/index.d.ts +897 -0
- package/dist/index.js +3088 -0
- package/dist/index.js.map +1 -0
- package/dist/invalidation.cjs +64 -0
- package/dist/invalidation.cjs.map +1 -0
- package/dist/invalidation.d.cts +52 -0
- package/dist/invalidation.d.ts +52 -0
- package/dist/invalidation.js +43 -0
- package/dist/invalidation.js.map +1 -0
- package/dist/json-format.cjs +319 -0
- package/dist/json-format.cjs.map +1 -0
- package/dist/json-format.d.cts +148 -0
- package/dist/json-format.d.ts +148 -0
- package/dist/json-format.js +286 -0
- package/dist/json-format.js.map +1 -0
- package/dist/latest-request.cjs +36 -0
- package/dist/latest-request.cjs.map +1 -0
- package/dist/latest-request.d.cts +29 -0
- package/dist/latest-request.d.ts +29 -0
- package/dist/latest-request.js +16 -0
- package/dist/latest-request.js.map +1 -0
- package/dist/qr.cjs +109 -0
- package/dist/qr.cjs.map +1 -0
- package/dist/qr.d.cts +47 -0
- package/dist/qr.d.ts +47 -0
- package/dist/qr.js +78 -0
- package/dist/qr.js.map +1 -0
- package/dist/react-tree.cjs +69 -0
- package/dist/react-tree.cjs.map +1 -0
- package/dist/react-tree.d.cts +38 -0
- package/dist/react-tree.d.ts +38 -0
- package/dist/react-tree.js +38 -0
- package/dist/react-tree.js.map +1 -0
- package/dist/search-scoring.cjs +116 -0
- package/dist/search-scoring.cjs.map +1 -0
- package/dist/search-scoring.d.cts +77 -0
- package/dist/search-scoring.d.ts +77 -0
- package/dist/search-scoring.js +95 -0
- package/dist/search-scoring.js.map +1 -0
- package/dist/text-case.cjs +313 -0
- package/dist/text-case.cjs.map +1 -0
- package/dist/text-case.d.cts +53 -0
- package/dist/text-case.d.ts +53 -0
- package/dist/text-case.js +292 -0
- package/dist/text-case.js.map +1 -0
- package/dist/toast.cjs +74 -0
- package/dist/toast.cjs.map +1 -0
- package/dist/toast.d.cts +92 -0
- package/dist/toast.d.ts +92 -0
- package/dist/toast.js +54 -0
- package/dist/toast.js.map +1 -0
- package/dist/url-state.cjs +212 -0
- package/dist/url-state.cjs.map +1 -0
- package/dist/url-state.d.cts +90 -0
- package/dist/url-state.d.ts +90 -0
- package/dist/url-state.js +199 -0
- package/dist/url-state.js.map +1 -0
- package/package.json +286 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/confirm.ts","../src/confirm/opener.ts","../src/confirm/host.tsx","../src/confirm/cn.ts","../src/confirm/alert-dialog.tsx","../src/react-tree.ts","../src/confirm/confirm-dialog.tsx"],"sourcesContent":["/**\n * @ai-matrx/kit/confirm — the confirm-dialog system. Browser dialogs are\n * banned; this is the replacement, both shapes:\n *\n * - Imperative: mount `<ConfirmDialogHost />` ONCE at app root, then from\n * anywhere: `if (!(await confirm({ title: \"Delete?\", variant: \"destructive\" }))) return;`\n * Calls queue until the host mounts; concurrent calls present sequentially.\n * - Declarative: `<ConfirmDialog open onOpenChange onConfirm ... />` inline,\n * when busy state is meaningful (a network delete holding the dialog open).\n *\n * Runtime deps of this subpath (and only this subpath's bundles):\n * `@radix-ui/react-alert-dialog` + `tailwind-merge`. Styling is the Matrx\n * Tailwind semantic-token vocabulary; hosts on other design systems override\n * via `className` / `contentClassName` (last-wins merge).\n */\nexport { confirm } from \"./confirm/opener\";\nexport type { ConfirmOptions } from \"./confirm/opener\";\nexport { ConfirmDialogHost } from \"./confirm/host\";\nexport { ConfirmDialog } from \"./confirm/confirm-dialog\";\nexport type { ConfirmDialogProps } from \"./confirm/confirm-dialog\";\n","/**\n * @ai-matrx/kit/confirm — imperative opener.\n *\n * Pure-TS imperative API for the global confirm dialog. Zero React, zero\n * dialog markup — this module is statically importable from anywhere\n * (hooks, utils, Redux thunks, async handlers, sync code, anything).\n *\n * The host (`ConfirmDialogHost`) registers a controller on mount and\n * unregisters on unmount. Calls made before the host has hydrated queue\n * up and resolve as soon as the host is alive — so a destructive action\n * triggered in the first ~50ms after page load still gets a real\n * confirmation, never a silent default-yes/no. With no host ever mounted,\n * a `confirm()` promise stays pending forever (the original's behavior —\n * it never resolves to a silent default).\n *\n * One dialog at a time: concurrent calls queue and present sequentially.\n *\n * Ported verbatim from matrx-frontend\n * `components/dialogs/confirm/confirmDialogOpener.ts`, with ONE structural\n * inversion: the host/queue state lives on `globalThis` under a\n * `Symbol.for` slot instead of module-level variables. With the package\n * built `splitting: false` in dual ESM/CJS format, this module is\n * duplicated into the root bundle and the `./confirm` bundle, and CJS/ESM\n * each instantiate their own module graph — a module-level variable would\n * silently split the host registration from the callers (the same hazard\n * `@ai-matrx/tap-target` documents for its link registry). Behavior is\n * unchanged; never \"clean this up\" into a module local.\n */\n\nimport type { ReactNode } from \"react\";\n\nexport interface ConfirmOptions {\n title: ReactNode;\n description?: ReactNode | undefined;\n confirmLabel?: string | undefined;\n /** `null` hides the cancel button (acknowledge-only dialogs). */\n cancelLabel?: string | null | undefined;\n variant?: \"default\" | \"destructive\" | undefined;\n}\n\ntype Resolver = (confirmed: boolean) => void;\n\ninterface PendingRequest {\n opts: ConfirmOptions;\n resolve: Resolver;\n}\n\ninterface HostController {\n show: (opts: ConfirmOptions, resolve: Resolver) => void;\n}\n\ninterface OpenerState {\n host: HostController | null;\n queue: PendingRequest[];\n}\n\nconst STATE_SLOT = Symbol.for(\"ai-matrx.kit.confirm-opener-state\");\n\nfunction getState(): OpenerState {\n const holder = globalThis as Record<symbol, OpenerState | undefined>;\n let state = holder[STATE_SLOT];\n if (!state) {\n state = { host: null, queue: [] };\n holder[STATE_SLOT] = state;\n }\n return state;\n}\n\n/** @internal Called by `ConfirmDialogHost` on mount. */\nexport function _registerHost(controller: HostController): void {\n const state = getState();\n state.host = controller;\n while (state.queue.length > 0) {\n const next = state.queue.shift()!;\n controller.show(next.opts, next.resolve);\n }\n}\n\n/** @internal Called by `ConfirmDialogHost` on unmount. */\nexport function _unregisterHost(controller: HostController): void {\n const state = getState();\n if (state.host === controller) state.host = null;\n}\n\n/** @internal Test-only: drop any registered host and pending queue. */\nexport function _resetConfirmOpenerState(): void {\n const state = getState();\n state.host = null;\n state.queue.length = 0;\n}\n\n/**\n * Imperative confirm. Returns a Promise that resolves `true` if the user\n * confirms, `false` if they cancel/dismiss. Replaces `window.confirm`.\n *\n * @example\n * const ok = await confirm({\n * title: \"Delete sandbox\",\n * description: \"This cannot be undone.\",\n * variant: \"destructive\",\n * confirmLabel: \"Delete\",\n * });\n * if (!ok) return;\n */\nexport function confirm(opts: ConfirmOptions): Promise<boolean> {\n return new Promise<boolean>((resolve) => {\n const state = getState();\n if (state.host) {\n state.host.show(opts, resolve);\n } else {\n state.queue.push({ opts, resolve });\n }\n });\n}\n","\"use client\";\n\n/**\n * `<ConfirmDialogHost />` — render ONCE, near the root of every provider\n * tree, so the imperative `confirm()` always has a live host to dispatch to.\n * Pre-mount calls queue inside `opener.ts` and resolve as soon as the host\n * registers.\n *\n * Ported from matrx-frontend `components/dialogs/confirm/\n * {ConfirmDialogHost,ConfirmDialogHostImpl}.tsx`, with the loading seam\n * inverted: the original split shell + impl and loaded the impl via\n * `next/dynamic({ ssr: false })` to keep radix out of route entry chunks.\n * A framework-agnostic package cannot use `next/dynamic`, so the host is\n * ONE directly-imported component; hosts that want the original's\n * code-splitting lazy-load the subpath themselves, e.g.\n * `dynamic(() => import(\"@ai-matrx/kit/confirm\").then(m => m.ConfirmDialogHost), { ssr: false })`.\n * The \"host renders `<ConfirmDialogHost/>` once\" contract is unchanged.\n *\n * Imperative model: calls to `confirm(...)` from anywhere push a request\n * into a ref-backed queue; this component drains the queue one item at a\n * time and renders a `<ConfirmDialog>` for the currently-active request.\n * Resolving Promise<boolean> happens on Confirm click (true), or on\n * dismiss/cancel (false). The dialog closes immediately on click — callers\n * that need an in-dialog busy spinner during async work should use the\n * inline `<ConfirmDialog>` with the `busy` prop instead.\n */\n\nimport * as React from \"react\";\n\nimport { ConfirmDialog } from \"./confirm-dialog\";\nimport {\n _registerHost,\n _unregisterHost,\n type ConfirmOptions,\n} from \"./opener\";\n\ninterface ActiveRequest {\n opts: ConfirmOptions;\n resolve: (confirmed: boolean) => void;\n}\n\nexport function ConfirmDialogHost() {\n const [active, setActive] = React.useState<ActiveRequest | null>(null);\n const [tick, setTick] = React.useState(0);\n const queueRef = React.useRef<ActiveRequest[]>([]);\n\n // Register/unregister the controller exactly once. The controller's\n // `show` always pushes onto the queue and bumps `tick`; the drain\n // effect below picks up from there. This avoids stale-closure bugs\n // around `active`.\n React.useEffect(() => {\n const controller = {\n show: (opts: ConfirmOptions, resolve: (confirmed: boolean) => void) => {\n queueRef.current.push({ opts, resolve });\n setTick((n) => n + 1);\n },\n };\n _registerHost(controller);\n return () => _unregisterHost(controller);\n }, []);\n\n // Drain the queue whenever nothing is showing.\n React.useEffect(() => {\n if (active === null && queueRef.current.length > 0) {\n setActive(queueRef.current.shift()!);\n }\n }, [active, tick]);\n\n const handleConfirm = React.useCallback(() => {\n if (!active) return;\n active.resolve(true);\n setActive(null);\n }, [active]);\n\n const handleOpenChange = React.useCallback(\n (open: boolean) => {\n if (!open && active) {\n active.resolve(false);\n setActive(null);\n }\n },\n [active],\n );\n\n return (\n <ConfirmDialog\n open={!!active}\n onOpenChange={handleOpenChange}\n title={active?.opts.title ?? \"\"}\n description={active?.opts.description}\n confirmLabel={active?.opts.confirmLabel}\n cancelLabel={active?.opts.cancelLabel}\n variant={active?.opts.variant}\n onConfirm={handleConfirm}\n />\n );\n}\n","import { twMerge } from \"tailwind-merge\";\n\n/**\n * Tailwind-aware className merge. The original app's `cn` is\n * `twMerge(clsx(inputs))`; here `clsx` is dropped (every call site passes\n * strings / false), but `tailwind-merge` is KEPT on purpose: the public\n * `className` / `contentClassName` overrides depend on last-wins conflict\n * resolution (e.g. a host's `max-w-3xl` must beat the built-in `max-w-lg`,\n * a destructive `bg-destructive` must beat the default `bg-primary`).\n * A naive join would leave both classes applied and let stylesheet order\n * decide — a real behavior divergence from the original.\n */\nexport function cn(\n ...values: Array<string | null | undefined | false>\n): string {\n return twMerge(values.filter(Boolean).join(\" \"));\n}\n","\"use client\";\n\n/**\n * Inlined shadcn-style wrapper over `@radix-ui/react-alert-dialog` — the one\n * real runtime dependency of the `./confirm` subpath (this subpath's product\n * IS the dialog). Ported from matrx-frontend `components/ui/alert-dialog.tsx`\n * with the host-shaped seams inverted:\n *\n * - `usePopoutContainer` (window-panels popout portal retargeting) is dropped:\n * the portal targets the Radix default (`document.body`). Hosts with exotic\n * portal needs pass `container` on `AlertDialogPortal` themselves.\n * - `buttonVariants` from the design system is inlined as the exact class\n * strings the two footer buttons use (base + default + outline variants,\n * design-system `button.tsx` as of this port). No cva dependency.\n * - Styling keeps the Tailwind semantic-token classes VERBATIM (`bg-background`,\n * `text-muted-foreground`, `bg-primary`, `border-border`, ...) — the platform\n * vocabulary. Hosts on other design systems override via the `className` /\n * `contentClassName` props (classes merge last-wins via tailwind-merge).\n *\n * THE ROOT RENDERS UNCONDITIONALLY — no mount gate. Radix ids come from\n * React's SSR-stable `useId`, so there is no SSR/client id mismatch to hide\n * from (the original's D144 ruling).\n */\n\nimport * as React from \"react\";\nimport * as AlertDialogPrimitive from \"@radix-ui/react-alert-dialog\";\n\nimport { cn } from \"./cn\";\nimport { treeContainsComponent } from \"../react-tree\";\n\n/**\n * Inlined design-system button classes (base + the two variants the alert\n * dialog footer uses). Source of truth while the originals live:\n * aidream `apps/shared/design-system/src/button.tsx`.\n */\nconst buttonBase =\n \"inline-flex cursor-pointer items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all duration-200 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-0 disabled:pointer-events-none disabled:opacity-50 active:scale-[0.98] [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0 h-9 px-4 py-2\";\nconst buttonDefault =\n \"bg-primary text-primary-foreground shadow-sm hover:bg-primary/90\";\nconst buttonOutline =\n \"border border-border bg-card shadow-sm hover:bg-accent hover:text-accent-foreground\";\n\nconst AlertDialog = AlertDialogPrimitive.Root;\n\nconst AlertDialogTrigger = AlertDialogPrimitive.Trigger;\n\nconst AlertDialogPortal = AlertDialogPrimitive.Portal;\n\nconst AlertDialogOverlay = React.forwardRef<\n React.ComponentRef<typeof AlertDialogPrimitive.Overlay>,\n React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Overlay>\n>(({ className, ...props }, ref) => (\n <AlertDialogPrimitive.Overlay\n className={cn(\n \"fixed inset-0 z-[10000] bg-black/80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0\",\n className,\n )}\n {...props}\n ref={ref}\n />\n));\nAlertDialogOverlay.displayName = AlertDialogPrimitive.Overlay.displayName;\n\n/**\n * Unstyled, non-portalling Content for custom AlertDialog layouts. AlertDialog\n * is always modal, so this keeps its ARIA semantics explicit and consistent.\n */\nconst AlertDialogContentPrimitive = React.forwardRef<\n React.ComponentRef<typeof AlertDialogPrimitive.Content>,\n React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Content>\n>(({ ...props }, ref) => (\n <AlertDialogPrimitive.Content {...props} ref={ref} aria-modal=\"true\" />\n));\nAlertDialogContentPrimitive.displayName = \"AlertDialogContentPrimitive\";\n\nconst AlertDialogDescription = React.forwardRef<\n React.ComponentRef<typeof AlertDialogPrimitive.Description>,\n React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Description>\n>(({ className, ...props }, ref) => (\n <AlertDialogPrimitive.Description\n ref={ref}\n className={cn(\"text-sm text-muted-foreground\", className)}\n {...props}\n />\n));\nAlertDialogDescription.displayName =\n AlertDialogPrimitive.Description.displayName;\n\nconst AlertDialogContent = React.forwardRef<\n React.ComponentRef<typeof AlertDialogPrimitive.Content>,\n React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Content>\n>(({ className, children, ...props }, ref) => {\n const hasDescription =\n treeContainsComponent(children, AlertDialogDescription) ||\n treeContainsComponent(children, AlertDialogPrimitive.Description);\n return (\n <AlertDialogPortal>\n <AlertDialogOverlay />\n <AlertDialogContentPrimitive\n ref={ref}\n className={cn(\n \"fixed left-[50%] top-[50%] z-[10000] grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border bg-background p-6 shadow-lg duration-200 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-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg\",\n className,\n )}\n {...props}\n >\n {!hasDescription && (\n <AlertDialogPrimitive.Description className=\"sr-only\">\n Please confirm the action described in this dialog.\n </AlertDialogPrimitive.Description>\n )}\n {children}\n </AlertDialogContentPrimitive>\n </AlertDialogPortal>\n );\n});\nAlertDialogContent.displayName = AlertDialogPrimitive.Content.displayName;\n\nconst AlertDialogHeader = ({\n className,\n ...props\n}: React.HTMLAttributes<HTMLDivElement>) => (\n <div\n className={cn(\n \"flex flex-col space-y-2 text-center sm:text-left\",\n className,\n )}\n {...props}\n />\n);\nAlertDialogHeader.displayName = \"AlertDialogHeader\";\n\nconst AlertDialogFooter = ({\n className,\n ...props\n}: React.HTMLAttributes<HTMLDivElement>) => (\n <div\n className={cn(\n \"flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2\",\n className,\n )}\n {...props}\n />\n);\nAlertDialogFooter.displayName = \"AlertDialogFooter\";\n\nconst AlertDialogTitle = React.forwardRef<\n React.ComponentRef<typeof AlertDialogPrimitive.Title>,\n React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Title>\n>(({ className, ...props }, ref) => (\n <AlertDialogPrimitive.Title\n ref={ref}\n className={cn(\"text-lg font-semibold\", className)}\n {...props}\n />\n));\nAlertDialogTitle.displayName = AlertDialogPrimitive.Title.displayName;\n\nconst AlertDialogAction = React.forwardRef<\n React.ComponentRef<typeof AlertDialogPrimitive.Action>,\n React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Action>\n>(({ className, ...props }, ref) => (\n <AlertDialogPrimitive.Action\n ref={ref}\n className={cn(buttonBase, buttonDefault, className)}\n {...props}\n />\n));\nAlertDialogAction.displayName = AlertDialogPrimitive.Action.displayName;\n\nconst AlertDialogCancel = React.forwardRef<\n React.ComponentRef<typeof AlertDialogPrimitive.Cancel>,\n React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Cancel>\n>(({ className, ...props }, ref) => (\n <AlertDialogPrimitive.Cancel\n ref={ref}\n className={cn(buttonBase, buttonOutline, \"mt-2 sm:mt-0\", className)}\n {...props}\n />\n));\nAlertDialogCancel.displayName = AlertDialogPrimitive.Cancel.displayName;\n\nexport {\n AlertDialog,\n AlertDialogPortal,\n AlertDialogOverlay,\n AlertDialogContentPrimitive,\n AlertDialogTrigger,\n AlertDialogContent,\n AlertDialogHeader,\n AlertDialogFooter,\n AlertDialogTitle,\n AlertDialogDescription,\n AlertDialogAction,\n AlertDialogCancel,\n};\n","/**\n * @ai-matrx/kit/react-tree — safe React children-tree scanning.\n *\n * Ported from matrx-frontend `lib/react/treeContainsComponent.ts`; this\n * subpath is the ONE home of the scanner inside the kit (the `/confirm`\n * subpath's alert-dialog imports it from here — no duplicate bodies).\n *\n * Two deliberate divergences from the frontend original, both documented:\n * - the dev-mode scream checks `typeof process` first, since this package may\n * load in an unbundled browser context where `process` is undefined;\n * - a React PORTAL child (`createPortal(...)` passed as a child) is a valid\n * React child, but it is not an element, an iterable, or a primitive — the\n * original fell through to the non-renderable branch, screamed a false\n * positive in dev, and skipped the portal's content. Portals are now\n * recognized and their children traversed.\n */\n\nimport * as React from \"react\";\n\nconst REACT_PORTAL_TYPE = Symbol.for(\"react.portal\");\n\n/**\n * Returns true if `Component` appears anywhere in the React element tree under\n * `node`. Used to detect optional a11y children (e.g. DialogDescription)\n * without rendering duplicates.\n *\n * DEFENSIVE + LOUD. A non-renderable node — a plain object or function passed\n * as a React child — is a real bug: React throws \"Objects are not valid as a\n * React child\" the instant it renders one. This a11y probe must NOT be the\n * crash site. `React.Children.toArray` would throw HERE, producing a trace that\n * points at the dialog primitive instead of the component that leaked the\n * object (this misdirection has burned real debugging hours). So we walk the\n * tree by hand, SKIP any non-renderable node, and scream in dev with its keys —\n * then let React report the defect at the true render site with the offending\n * component in the stack. For every VALID tree the result is identical to the\n * old `React.Children.toArray(node).some(...)`.\n */\nexport function treeContainsComponent(\n node: React.ReactNode,\n Component: React.ElementType,\n): boolean {\n if (node == null || typeof node === \"boolean\") return false;\n\n if (Array.isArray(node)) {\n return node.some((child) => treeContainsComponent(child, Component));\n }\n\n if (React.isValidElement(node)) {\n if (node.type === Component) return true;\n const props = node.props as { children?: React.ReactNode };\n return props.children != null\n ? treeContainsComponent(props.children, Component)\n : false;\n }\n\n // Strings / numbers are valid leaf children but never the Component.\n if (typeof node === \"string\" || typeof node === \"number\") return false;\n\n // A portal is a valid child that is NOT an element: traverse its content.\n if (\n typeof node === \"object\" &&\n (node as { $$typeof?: unknown }).$$typeof === REACT_PORTAL_TYPE\n ) {\n return treeContainsComponent(\n (node as { children?: React.ReactNode }).children,\n Component,\n );\n }\n\n // Non-array iterables (Set, Map, generator) are valid React children — React\n // supports them — so traverse rather than reject.\n if (typeof node === \"object\" && Symbol.iterator in node) {\n return Array.from(node as Iterable<React.ReactNode>).some((child) =>\n treeContainsComponent(child, Component),\n );\n }\n\n // Anything else (a raw object, a function) is NOT a valid React child. React\n // will throw when it renders this; we must not throw first and hide the cause.\n if (\n typeof process !== \"undefined\" &&\n process.env.NODE_ENV !== \"production\"\n ) {\n const keys =\n typeof node === \"object\"\n ? ` with keys {${Object.keys(node).join(\", \")}}`\n : \"\";\n console.error(\n `[treeContainsComponent] A non-renderable value${keys} is being passed as a React child. ` +\n \"React will throw 'Objects are not valid as a React child' at the real render site. \" +\n \"Stringify it (e.g. JSON.stringify) before rendering.\",\n node,\n );\n }\n return false;\n}\n","\"use client\";\n\n/**\n * Declarative `<ConfirmDialog />` — drop-in replacement for `window.confirm`.\n * Ported verbatim from matrx-frontend `components/ui/confirm-dialog.tsx`;\n * the only inversion is the busy spinner: `Loader2` from lucide-react is\n * inlined as a single SVG (the `@ai-matrx/tap-target` precedent — one icon\n * does not justify an icon dependency). Path annotated below.\n *\n * Pattern: hold the pending target in state, render <ConfirmDialog />\n * once at the bottom of the component, and open it by setting the target.\n * When busy state is meaningful (e.g. a network delete that should hold the\n * dialog open with a spinner), use THIS component inline; the imperative\n * `confirm()` closes immediately on click.\n */\n\nimport * as React from \"react\";\n\nimport { cn } from \"./cn\";\nimport {\n AlertDialog,\n AlertDialogAction,\n AlertDialogCancel,\n AlertDialogContent,\n AlertDialogDescription,\n AlertDialogFooter,\n AlertDialogHeader,\n AlertDialogTitle,\n} from \"./alert-dialog\";\n\n/** lucide `loader-circle` (a.k.a. `Loader2`) v1.22.0, inlined. */\nfunction SpinnerIcon({ className }: { className?: string }) {\n return (\n <svg\n xmlns=\"http://www.w3.org/2000/svg\"\n width={24}\n height={24}\n viewBox=\"0 0 24 24\"\n fill=\"none\"\n stroke=\"currentColor\"\n strokeWidth={2}\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n aria-hidden=\"true\"\n className={className}\n >\n <path d=\"M21 12a9 9 0 1 1-6.219-8.56\" />\n </svg>\n );\n}\n\nexport interface ConfirmDialogProps {\n open: boolean;\n onOpenChange: (open: boolean) => void;\n title: React.ReactNode;\n description?: React.ReactNode | undefined;\n /**\n * Rich body rendered between the header and the footer, OUTSIDE the\n * description `<p>` — use for block-level content (diffs, previews, lists)\n * that would be invalid HTML inside `description`.\n */\n content?: React.ReactNode | undefined;\n /** Extra classes for the dialog content (e.g. a wider max-w for diffs). */\n contentClassName?: string | undefined;\n confirmLabel?: string | undefined;\n /**\n * `null` hides the cancel button entirely — for acknowledge-only dialogs\n * where there is nothing to cancel. Anything else labels it.\n */\n cancelLabel?: string | null | undefined;\n variant?: \"default\" | \"destructive\" | undefined;\n busy?: boolean | undefined;\n /**\n * Blocks confirming without pretending work is in flight. For a dialog whose\n * `content` asks the user something the action cannot proceed without — the\n * choice is missing, not loading — `busy` would show a misleading spinner.\n */\n confirmDisabled?: boolean | undefined;\n onConfirm: () => void | Promise<void>;\n}\n\n/**\n * Drop-in replacement for `window.confirm`. Use this anywhere you would\n * otherwise reach for a browser-level confirm dialog.\n */\nexport function ConfirmDialog({\n open,\n onOpenChange,\n title,\n description,\n content,\n contentClassName,\n confirmLabel = \"Confirm\",\n cancelLabel = \"Cancel\",\n variant = \"default\",\n busy = false,\n confirmDisabled = false,\n onConfirm,\n}: ConfirmDialogProps) {\n return (\n <AlertDialog open={open} onOpenChange={onOpenChange}>\n <AlertDialogContent className={contentClassName}>\n <AlertDialogHeader>\n <AlertDialogTitle>{title}</AlertDialogTitle>\n {description ? (\n <AlertDialogDescription>{description}</AlertDialogDescription>\n ) : null}\n </AlertDialogHeader>\n {content ?? null}\n <AlertDialogFooter>\n {cancelLabel === null ? null : (\n <AlertDialogCancel className=\"max-lg:min-h-11\" disabled={busy}>\n {cancelLabel}\n </AlertDialogCancel>\n )}\n <AlertDialogAction\n disabled={busy || confirmDisabled}\n onClick={(event) => {\n event.preventDefault();\n void onConfirm();\n }}\n className={cn(\n \"max-lg:min-h-11\",\n variant === \"destructive\" &&\n \"bg-destructive text-destructive-foreground hover:bg-destructive/90\",\n )}\n >\n {busy ? <SpinnerIcon className=\"mr-2 h-4 w-4 animate-spin\" /> : null}\n {confirmLabel}\n </AlertDialogAction>\n </AlertDialogFooter>\n </AlertDialogContent>\n </AlertDialog>\n );\n}\n"],"mappings":"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","names":["React","React","import_jsx_runtime","import_jsx_runtime"]}
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
import { ReactNode } from 'react';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* @ai-matrx/kit/confirm — imperative opener.
|
|
6
|
+
*
|
|
7
|
+
* Pure-TS imperative API for the global confirm dialog. Zero React, zero
|
|
8
|
+
* dialog markup — this module is statically importable from anywhere
|
|
9
|
+
* (hooks, utils, Redux thunks, async handlers, sync code, anything).
|
|
10
|
+
*
|
|
11
|
+
* The host (`ConfirmDialogHost`) registers a controller on mount and
|
|
12
|
+
* unregisters on unmount. Calls made before the host has hydrated queue
|
|
13
|
+
* up and resolve as soon as the host is alive — so a destructive action
|
|
14
|
+
* triggered in the first ~50ms after page load still gets a real
|
|
15
|
+
* confirmation, never a silent default-yes/no. With no host ever mounted,
|
|
16
|
+
* a `confirm()` promise stays pending forever (the original's behavior —
|
|
17
|
+
* it never resolves to a silent default).
|
|
18
|
+
*
|
|
19
|
+
* One dialog at a time: concurrent calls queue and present sequentially.
|
|
20
|
+
*
|
|
21
|
+
* Ported verbatim from matrx-frontend
|
|
22
|
+
* `components/dialogs/confirm/confirmDialogOpener.ts`, with ONE structural
|
|
23
|
+
* inversion: the host/queue state lives on `globalThis` under a
|
|
24
|
+
* `Symbol.for` slot instead of module-level variables. With the package
|
|
25
|
+
* built `splitting: false` in dual ESM/CJS format, this module is
|
|
26
|
+
* duplicated into the root bundle and the `./confirm` bundle, and CJS/ESM
|
|
27
|
+
* each instantiate their own module graph — a module-level variable would
|
|
28
|
+
* silently split the host registration from the callers (the same hazard
|
|
29
|
+
* `@ai-matrx/tap-target` documents for its link registry). Behavior is
|
|
30
|
+
* unchanged; never "clean this up" into a module local.
|
|
31
|
+
*/
|
|
32
|
+
|
|
33
|
+
interface ConfirmOptions {
|
|
34
|
+
title: ReactNode;
|
|
35
|
+
description?: ReactNode | undefined;
|
|
36
|
+
confirmLabel?: string | undefined;
|
|
37
|
+
/** `null` hides the cancel button (acknowledge-only dialogs). */
|
|
38
|
+
cancelLabel?: string | null | undefined;
|
|
39
|
+
variant?: "default" | "destructive" | undefined;
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Imperative confirm. Returns a Promise that resolves `true` if the user
|
|
43
|
+
* confirms, `false` if they cancel/dismiss. Replaces `window.confirm`.
|
|
44
|
+
*
|
|
45
|
+
* @example
|
|
46
|
+
* const ok = await confirm({
|
|
47
|
+
* title: "Delete sandbox",
|
|
48
|
+
* description: "This cannot be undone.",
|
|
49
|
+
* variant: "destructive",
|
|
50
|
+
* confirmLabel: "Delete",
|
|
51
|
+
* });
|
|
52
|
+
* if (!ok) return;
|
|
53
|
+
*/
|
|
54
|
+
declare function confirm(opts: ConfirmOptions): Promise<boolean>;
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* `<ConfirmDialogHost />` — render ONCE, near the root of every provider
|
|
58
|
+
* tree, so the imperative `confirm()` always has a live host to dispatch to.
|
|
59
|
+
* Pre-mount calls queue inside `opener.ts` and resolve as soon as the host
|
|
60
|
+
* registers.
|
|
61
|
+
*
|
|
62
|
+
* Ported from matrx-frontend `components/dialogs/confirm/
|
|
63
|
+
* {ConfirmDialogHost,ConfirmDialogHostImpl}.tsx`, with the loading seam
|
|
64
|
+
* inverted: the original split shell + impl and loaded the impl via
|
|
65
|
+
* `next/dynamic({ ssr: false })` to keep radix out of route entry chunks.
|
|
66
|
+
* A framework-agnostic package cannot use `next/dynamic`, so the host is
|
|
67
|
+
* ONE directly-imported component; hosts that want the original's
|
|
68
|
+
* code-splitting lazy-load the subpath themselves, e.g.
|
|
69
|
+
* `dynamic(() => import("@ai-matrx/kit/confirm").then(m => m.ConfirmDialogHost), { ssr: false })`.
|
|
70
|
+
* The "host renders `<ConfirmDialogHost/>` once" contract is unchanged.
|
|
71
|
+
*
|
|
72
|
+
* Imperative model: calls to `confirm(...)` from anywhere push a request
|
|
73
|
+
* into a ref-backed queue; this component drains the queue one item at a
|
|
74
|
+
* time and renders a `<ConfirmDialog>` for the currently-active request.
|
|
75
|
+
* Resolving Promise<boolean> happens on Confirm click (true), or on
|
|
76
|
+
* dismiss/cancel (false). The dialog closes immediately on click — callers
|
|
77
|
+
* that need an in-dialog busy spinner during async work should use the
|
|
78
|
+
* inline `<ConfirmDialog>` with the `busy` prop instead.
|
|
79
|
+
*/
|
|
80
|
+
|
|
81
|
+
declare function ConfirmDialogHost(): React.JSX.Element;
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Declarative `<ConfirmDialog />` — drop-in replacement for `window.confirm`.
|
|
85
|
+
* Ported verbatim from matrx-frontend `components/ui/confirm-dialog.tsx`;
|
|
86
|
+
* the only inversion is the busy spinner: `Loader2` from lucide-react is
|
|
87
|
+
* inlined as a single SVG (the `@ai-matrx/tap-target` precedent — one icon
|
|
88
|
+
* does not justify an icon dependency). Path annotated below.
|
|
89
|
+
*
|
|
90
|
+
* Pattern: hold the pending target in state, render <ConfirmDialog />
|
|
91
|
+
* once at the bottom of the component, and open it by setting the target.
|
|
92
|
+
* When busy state is meaningful (e.g. a network delete that should hold the
|
|
93
|
+
* dialog open with a spinner), use THIS component inline; the imperative
|
|
94
|
+
* `confirm()` closes immediately on click.
|
|
95
|
+
*/
|
|
96
|
+
|
|
97
|
+
interface ConfirmDialogProps {
|
|
98
|
+
open: boolean;
|
|
99
|
+
onOpenChange: (open: boolean) => void;
|
|
100
|
+
title: React.ReactNode;
|
|
101
|
+
description?: React.ReactNode | undefined;
|
|
102
|
+
/**
|
|
103
|
+
* Rich body rendered between the header and the footer, OUTSIDE the
|
|
104
|
+
* description `<p>` — use for block-level content (diffs, previews, lists)
|
|
105
|
+
* that would be invalid HTML inside `description`.
|
|
106
|
+
*/
|
|
107
|
+
content?: React.ReactNode | undefined;
|
|
108
|
+
/** Extra classes for the dialog content (e.g. a wider max-w for diffs). */
|
|
109
|
+
contentClassName?: string | undefined;
|
|
110
|
+
confirmLabel?: string | undefined;
|
|
111
|
+
/**
|
|
112
|
+
* `null` hides the cancel button entirely — for acknowledge-only dialogs
|
|
113
|
+
* where there is nothing to cancel. Anything else labels it.
|
|
114
|
+
*/
|
|
115
|
+
cancelLabel?: string | null | undefined;
|
|
116
|
+
variant?: "default" | "destructive" | undefined;
|
|
117
|
+
busy?: boolean | undefined;
|
|
118
|
+
/**
|
|
119
|
+
* Blocks confirming without pretending work is in flight. For a dialog whose
|
|
120
|
+
* `content` asks the user something the action cannot proceed without — the
|
|
121
|
+
* choice is missing, not loading — `busy` would show a misleading spinner.
|
|
122
|
+
*/
|
|
123
|
+
confirmDisabled?: boolean | undefined;
|
|
124
|
+
onConfirm: () => void | Promise<void>;
|
|
125
|
+
}
|
|
126
|
+
/**
|
|
127
|
+
* Drop-in replacement for `window.confirm`. Use this anywhere you would
|
|
128
|
+
* otherwise reach for a browser-level confirm dialog.
|
|
129
|
+
*/
|
|
130
|
+
declare function ConfirmDialog({ open, onOpenChange, title, description, content, contentClassName, confirmLabel, cancelLabel, variant, busy, confirmDisabled, onConfirm, }: ConfirmDialogProps): React.JSX.Element;
|
|
131
|
+
|
|
132
|
+
export { ConfirmDialog, ConfirmDialogHost, type ConfirmDialogProps, type ConfirmOptions, confirm };
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
import { ReactNode } from 'react';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* @ai-matrx/kit/confirm — imperative opener.
|
|
6
|
+
*
|
|
7
|
+
* Pure-TS imperative API for the global confirm dialog. Zero React, zero
|
|
8
|
+
* dialog markup — this module is statically importable from anywhere
|
|
9
|
+
* (hooks, utils, Redux thunks, async handlers, sync code, anything).
|
|
10
|
+
*
|
|
11
|
+
* The host (`ConfirmDialogHost`) registers a controller on mount and
|
|
12
|
+
* unregisters on unmount. Calls made before the host has hydrated queue
|
|
13
|
+
* up and resolve as soon as the host is alive — so a destructive action
|
|
14
|
+
* triggered in the first ~50ms after page load still gets a real
|
|
15
|
+
* confirmation, never a silent default-yes/no. With no host ever mounted,
|
|
16
|
+
* a `confirm()` promise stays pending forever (the original's behavior —
|
|
17
|
+
* it never resolves to a silent default).
|
|
18
|
+
*
|
|
19
|
+
* One dialog at a time: concurrent calls queue and present sequentially.
|
|
20
|
+
*
|
|
21
|
+
* Ported verbatim from matrx-frontend
|
|
22
|
+
* `components/dialogs/confirm/confirmDialogOpener.ts`, with ONE structural
|
|
23
|
+
* inversion: the host/queue state lives on `globalThis` under a
|
|
24
|
+
* `Symbol.for` slot instead of module-level variables. With the package
|
|
25
|
+
* built `splitting: false` in dual ESM/CJS format, this module is
|
|
26
|
+
* duplicated into the root bundle and the `./confirm` bundle, and CJS/ESM
|
|
27
|
+
* each instantiate their own module graph — a module-level variable would
|
|
28
|
+
* silently split the host registration from the callers (the same hazard
|
|
29
|
+
* `@ai-matrx/tap-target` documents for its link registry). Behavior is
|
|
30
|
+
* unchanged; never "clean this up" into a module local.
|
|
31
|
+
*/
|
|
32
|
+
|
|
33
|
+
interface ConfirmOptions {
|
|
34
|
+
title: ReactNode;
|
|
35
|
+
description?: ReactNode | undefined;
|
|
36
|
+
confirmLabel?: string | undefined;
|
|
37
|
+
/** `null` hides the cancel button (acknowledge-only dialogs). */
|
|
38
|
+
cancelLabel?: string | null | undefined;
|
|
39
|
+
variant?: "default" | "destructive" | undefined;
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Imperative confirm. Returns a Promise that resolves `true` if the user
|
|
43
|
+
* confirms, `false` if they cancel/dismiss. Replaces `window.confirm`.
|
|
44
|
+
*
|
|
45
|
+
* @example
|
|
46
|
+
* const ok = await confirm({
|
|
47
|
+
* title: "Delete sandbox",
|
|
48
|
+
* description: "This cannot be undone.",
|
|
49
|
+
* variant: "destructive",
|
|
50
|
+
* confirmLabel: "Delete",
|
|
51
|
+
* });
|
|
52
|
+
* if (!ok) return;
|
|
53
|
+
*/
|
|
54
|
+
declare function confirm(opts: ConfirmOptions): Promise<boolean>;
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* `<ConfirmDialogHost />` — render ONCE, near the root of every provider
|
|
58
|
+
* tree, so the imperative `confirm()` always has a live host to dispatch to.
|
|
59
|
+
* Pre-mount calls queue inside `opener.ts` and resolve as soon as the host
|
|
60
|
+
* registers.
|
|
61
|
+
*
|
|
62
|
+
* Ported from matrx-frontend `components/dialogs/confirm/
|
|
63
|
+
* {ConfirmDialogHost,ConfirmDialogHostImpl}.tsx`, with the loading seam
|
|
64
|
+
* inverted: the original split shell + impl and loaded the impl via
|
|
65
|
+
* `next/dynamic({ ssr: false })` to keep radix out of route entry chunks.
|
|
66
|
+
* A framework-agnostic package cannot use `next/dynamic`, so the host is
|
|
67
|
+
* ONE directly-imported component; hosts that want the original's
|
|
68
|
+
* code-splitting lazy-load the subpath themselves, e.g.
|
|
69
|
+
* `dynamic(() => import("@ai-matrx/kit/confirm").then(m => m.ConfirmDialogHost), { ssr: false })`.
|
|
70
|
+
* The "host renders `<ConfirmDialogHost/>` once" contract is unchanged.
|
|
71
|
+
*
|
|
72
|
+
* Imperative model: calls to `confirm(...)` from anywhere push a request
|
|
73
|
+
* into a ref-backed queue; this component drains the queue one item at a
|
|
74
|
+
* time and renders a `<ConfirmDialog>` for the currently-active request.
|
|
75
|
+
* Resolving Promise<boolean> happens on Confirm click (true), or on
|
|
76
|
+
* dismiss/cancel (false). The dialog closes immediately on click — callers
|
|
77
|
+
* that need an in-dialog busy spinner during async work should use the
|
|
78
|
+
* inline `<ConfirmDialog>` with the `busy` prop instead.
|
|
79
|
+
*/
|
|
80
|
+
|
|
81
|
+
declare function ConfirmDialogHost(): React.JSX.Element;
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Declarative `<ConfirmDialog />` — drop-in replacement for `window.confirm`.
|
|
85
|
+
* Ported verbatim from matrx-frontend `components/ui/confirm-dialog.tsx`;
|
|
86
|
+
* the only inversion is the busy spinner: `Loader2` from lucide-react is
|
|
87
|
+
* inlined as a single SVG (the `@ai-matrx/tap-target` precedent — one icon
|
|
88
|
+
* does not justify an icon dependency). Path annotated below.
|
|
89
|
+
*
|
|
90
|
+
* Pattern: hold the pending target in state, render <ConfirmDialog />
|
|
91
|
+
* once at the bottom of the component, and open it by setting the target.
|
|
92
|
+
* When busy state is meaningful (e.g. a network delete that should hold the
|
|
93
|
+
* dialog open with a spinner), use THIS component inline; the imperative
|
|
94
|
+
* `confirm()` closes immediately on click.
|
|
95
|
+
*/
|
|
96
|
+
|
|
97
|
+
interface ConfirmDialogProps {
|
|
98
|
+
open: boolean;
|
|
99
|
+
onOpenChange: (open: boolean) => void;
|
|
100
|
+
title: React.ReactNode;
|
|
101
|
+
description?: React.ReactNode | undefined;
|
|
102
|
+
/**
|
|
103
|
+
* Rich body rendered between the header and the footer, OUTSIDE the
|
|
104
|
+
* description `<p>` — use for block-level content (diffs, previews, lists)
|
|
105
|
+
* that would be invalid HTML inside `description`.
|
|
106
|
+
*/
|
|
107
|
+
content?: React.ReactNode | undefined;
|
|
108
|
+
/** Extra classes for the dialog content (e.g. a wider max-w for diffs). */
|
|
109
|
+
contentClassName?: string | undefined;
|
|
110
|
+
confirmLabel?: string | undefined;
|
|
111
|
+
/**
|
|
112
|
+
* `null` hides the cancel button entirely — for acknowledge-only dialogs
|
|
113
|
+
* where there is nothing to cancel. Anything else labels it.
|
|
114
|
+
*/
|
|
115
|
+
cancelLabel?: string | null | undefined;
|
|
116
|
+
variant?: "default" | "destructive" | undefined;
|
|
117
|
+
busy?: boolean | undefined;
|
|
118
|
+
/**
|
|
119
|
+
* Blocks confirming without pretending work is in flight. For a dialog whose
|
|
120
|
+
* `content` asks the user something the action cannot proceed without — the
|
|
121
|
+
* choice is missing, not loading — `busy` would show a misleading spinner.
|
|
122
|
+
*/
|
|
123
|
+
confirmDisabled?: boolean | undefined;
|
|
124
|
+
onConfirm: () => void | Promise<void>;
|
|
125
|
+
}
|
|
126
|
+
/**
|
|
127
|
+
* Drop-in replacement for `window.confirm`. Use this anywhere you would
|
|
128
|
+
* otherwise reach for a browser-level confirm dialog.
|
|
129
|
+
*/
|
|
130
|
+
declare function ConfirmDialog({ open, onOpenChange, title, description, content, contentClassName, confirmLabel, cancelLabel, variant, busy, confirmDisabled, onConfirm, }: ConfirmDialogProps): React.JSX.Element;
|
|
131
|
+
|
|
132
|
+
export { ConfirmDialog, ConfirmDialogHost, type ConfirmDialogProps, type ConfirmOptions, confirm };
|
package/dist/confirm.js
ADDED
|
@@ -0,0 +1,312 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
// src/confirm/opener.ts
|
|
4
|
+
var STATE_SLOT = /* @__PURE__ */ Symbol.for("ai-matrx.kit.confirm-opener-state");
|
|
5
|
+
function getState() {
|
|
6
|
+
const holder = globalThis;
|
|
7
|
+
let state = holder[STATE_SLOT];
|
|
8
|
+
if (!state) {
|
|
9
|
+
state = { host: null, queue: [] };
|
|
10
|
+
holder[STATE_SLOT] = state;
|
|
11
|
+
}
|
|
12
|
+
return state;
|
|
13
|
+
}
|
|
14
|
+
function _registerHost(controller) {
|
|
15
|
+
const state = getState();
|
|
16
|
+
state.host = controller;
|
|
17
|
+
while (state.queue.length > 0) {
|
|
18
|
+
const next = state.queue.shift();
|
|
19
|
+
controller.show(next.opts, next.resolve);
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
function _unregisterHost(controller) {
|
|
23
|
+
const state = getState();
|
|
24
|
+
if (state.host === controller) state.host = null;
|
|
25
|
+
}
|
|
26
|
+
function confirm(opts) {
|
|
27
|
+
return new Promise((resolve) => {
|
|
28
|
+
const state = getState();
|
|
29
|
+
if (state.host) {
|
|
30
|
+
state.host.show(opts, resolve);
|
|
31
|
+
} else {
|
|
32
|
+
state.queue.push({ opts, resolve });
|
|
33
|
+
}
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// src/confirm/host.tsx
|
|
38
|
+
import * as React3 from "react";
|
|
39
|
+
|
|
40
|
+
// src/confirm/cn.ts
|
|
41
|
+
import { twMerge } from "tailwind-merge";
|
|
42
|
+
function cn(...values) {
|
|
43
|
+
return twMerge(values.filter(Boolean).join(" "));
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// src/confirm/alert-dialog.tsx
|
|
47
|
+
import * as React2 from "react";
|
|
48
|
+
import * as AlertDialogPrimitive from "@radix-ui/react-alert-dialog";
|
|
49
|
+
|
|
50
|
+
// src/react-tree.ts
|
|
51
|
+
import * as React from "react";
|
|
52
|
+
var REACT_PORTAL_TYPE = /* @__PURE__ */ Symbol.for("react.portal");
|
|
53
|
+
function treeContainsComponent(node, Component) {
|
|
54
|
+
if (node == null || typeof node === "boolean") return false;
|
|
55
|
+
if (Array.isArray(node)) {
|
|
56
|
+
return node.some((child) => treeContainsComponent(child, Component));
|
|
57
|
+
}
|
|
58
|
+
if (React.isValidElement(node)) {
|
|
59
|
+
if (node.type === Component) return true;
|
|
60
|
+
const props = node.props;
|
|
61
|
+
return props.children != null ? treeContainsComponent(props.children, Component) : false;
|
|
62
|
+
}
|
|
63
|
+
if (typeof node === "string" || typeof node === "number") return false;
|
|
64
|
+
if (typeof node === "object" && node.$$typeof === REACT_PORTAL_TYPE) {
|
|
65
|
+
return treeContainsComponent(
|
|
66
|
+
node.children,
|
|
67
|
+
Component
|
|
68
|
+
);
|
|
69
|
+
}
|
|
70
|
+
if (typeof node === "object" && Symbol.iterator in node) {
|
|
71
|
+
return Array.from(node).some(
|
|
72
|
+
(child) => treeContainsComponent(child, Component)
|
|
73
|
+
);
|
|
74
|
+
}
|
|
75
|
+
if (typeof process !== "undefined" && true) {
|
|
76
|
+
const keys = typeof node === "object" ? ` with keys {${Object.keys(node).join(", ")}}` : "";
|
|
77
|
+
console.error(
|
|
78
|
+
`[treeContainsComponent] A non-renderable value${keys} is being passed as a React child. React will throw 'Objects are not valid as a React child' at the real render site. Stringify it (e.g. JSON.stringify) before rendering.`,
|
|
79
|
+
node
|
|
80
|
+
);
|
|
81
|
+
}
|
|
82
|
+
return false;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// src/confirm/alert-dialog.tsx
|
|
86
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
87
|
+
var buttonBase = "inline-flex cursor-pointer items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all duration-200 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-0 disabled:pointer-events-none disabled:opacity-50 active:scale-[0.98] [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0 h-9 px-4 py-2";
|
|
88
|
+
var buttonDefault = "bg-primary text-primary-foreground shadow-sm hover:bg-primary/90";
|
|
89
|
+
var buttonOutline = "border border-border bg-card shadow-sm hover:bg-accent hover:text-accent-foreground";
|
|
90
|
+
var AlertDialog = AlertDialogPrimitive.Root;
|
|
91
|
+
var AlertDialogPortal = AlertDialogPrimitive.Portal;
|
|
92
|
+
var AlertDialogOverlay = React2.forwardRef(({ className, ...props }, ref) => /* @__PURE__ */ jsx(
|
|
93
|
+
AlertDialogPrimitive.Overlay,
|
|
94
|
+
{
|
|
95
|
+
className: cn(
|
|
96
|
+
"fixed inset-0 z-[10000] bg-black/80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
|
|
97
|
+
className
|
|
98
|
+
),
|
|
99
|
+
...props,
|
|
100
|
+
ref
|
|
101
|
+
}
|
|
102
|
+
));
|
|
103
|
+
AlertDialogOverlay.displayName = AlertDialogPrimitive.Overlay.displayName;
|
|
104
|
+
var AlertDialogContentPrimitive = React2.forwardRef(({ ...props }, ref) => /* @__PURE__ */ jsx(AlertDialogPrimitive.Content, { ...props, ref, "aria-modal": "true" }));
|
|
105
|
+
AlertDialogContentPrimitive.displayName = "AlertDialogContentPrimitive";
|
|
106
|
+
var AlertDialogDescription = React2.forwardRef(({ className, ...props }, ref) => /* @__PURE__ */ jsx(
|
|
107
|
+
AlertDialogPrimitive.Description,
|
|
108
|
+
{
|
|
109
|
+
ref,
|
|
110
|
+
className: cn("text-sm text-muted-foreground", className),
|
|
111
|
+
...props
|
|
112
|
+
}
|
|
113
|
+
));
|
|
114
|
+
AlertDialogDescription.displayName = AlertDialogPrimitive.Description.displayName;
|
|
115
|
+
var AlertDialogContent = React2.forwardRef(({ className, children, ...props }, ref) => {
|
|
116
|
+
const hasDescription = treeContainsComponent(children, AlertDialogDescription) || treeContainsComponent(children, AlertDialogPrimitive.Description);
|
|
117
|
+
return /* @__PURE__ */ jsxs(AlertDialogPortal, { children: [
|
|
118
|
+
/* @__PURE__ */ jsx(AlertDialogOverlay, {}),
|
|
119
|
+
/* @__PURE__ */ jsxs(
|
|
120
|
+
AlertDialogContentPrimitive,
|
|
121
|
+
{
|
|
122
|
+
ref,
|
|
123
|
+
className: cn(
|
|
124
|
+
"fixed left-[50%] top-[50%] z-[10000] grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border bg-background p-6 shadow-lg duration-200 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-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg",
|
|
125
|
+
className
|
|
126
|
+
),
|
|
127
|
+
...props,
|
|
128
|
+
children: [
|
|
129
|
+
!hasDescription && /* @__PURE__ */ jsx(AlertDialogPrimitive.Description, { className: "sr-only", children: "Please confirm the action described in this dialog." }),
|
|
130
|
+
children
|
|
131
|
+
]
|
|
132
|
+
}
|
|
133
|
+
)
|
|
134
|
+
] });
|
|
135
|
+
});
|
|
136
|
+
AlertDialogContent.displayName = AlertDialogPrimitive.Content.displayName;
|
|
137
|
+
var AlertDialogHeader = ({
|
|
138
|
+
className,
|
|
139
|
+
...props
|
|
140
|
+
}) => /* @__PURE__ */ jsx(
|
|
141
|
+
"div",
|
|
142
|
+
{
|
|
143
|
+
className: cn(
|
|
144
|
+
"flex flex-col space-y-2 text-center sm:text-left",
|
|
145
|
+
className
|
|
146
|
+
),
|
|
147
|
+
...props
|
|
148
|
+
}
|
|
149
|
+
);
|
|
150
|
+
AlertDialogHeader.displayName = "AlertDialogHeader";
|
|
151
|
+
var AlertDialogFooter = ({
|
|
152
|
+
className,
|
|
153
|
+
...props
|
|
154
|
+
}) => /* @__PURE__ */ jsx(
|
|
155
|
+
"div",
|
|
156
|
+
{
|
|
157
|
+
className: cn(
|
|
158
|
+
"flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2",
|
|
159
|
+
className
|
|
160
|
+
),
|
|
161
|
+
...props
|
|
162
|
+
}
|
|
163
|
+
);
|
|
164
|
+
AlertDialogFooter.displayName = "AlertDialogFooter";
|
|
165
|
+
var AlertDialogTitle = React2.forwardRef(({ className, ...props }, ref) => /* @__PURE__ */ jsx(
|
|
166
|
+
AlertDialogPrimitive.Title,
|
|
167
|
+
{
|
|
168
|
+
ref,
|
|
169
|
+
className: cn("text-lg font-semibold", className),
|
|
170
|
+
...props
|
|
171
|
+
}
|
|
172
|
+
));
|
|
173
|
+
AlertDialogTitle.displayName = AlertDialogPrimitive.Title.displayName;
|
|
174
|
+
var AlertDialogAction = React2.forwardRef(({ className, ...props }, ref) => /* @__PURE__ */ jsx(
|
|
175
|
+
AlertDialogPrimitive.Action,
|
|
176
|
+
{
|
|
177
|
+
ref,
|
|
178
|
+
className: cn(buttonBase, buttonDefault, className),
|
|
179
|
+
...props
|
|
180
|
+
}
|
|
181
|
+
));
|
|
182
|
+
AlertDialogAction.displayName = AlertDialogPrimitive.Action.displayName;
|
|
183
|
+
var AlertDialogCancel = React2.forwardRef(({ className, ...props }, ref) => /* @__PURE__ */ jsx(
|
|
184
|
+
AlertDialogPrimitive.Cancel,
|
|
185
|
+
{
|
|
186
|
+
ref,
|
|
187
|
+
className: cn(buttonBase, buttonOutline, "mt-2 sm:mt-0", className),
|
|
188
|
+
...props
|
|
189
|
+
}
|
|
190
|
+
));
|
|
191
|
+
AlertDialogCancel.displayName = AlertDialogPrimitive.Cancel.displayName;
|
|
192
|
+
|
|
193
|
+
// src/confirm/confirm-dialog.tsx
|
|
194
|
+
import { jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
|
|
195
|
+
function SpinnerIcon({ className }) {
|
|
196
|
+
return /* @__PURE__ */ jsx2(
|
|
197
|
+
"svg",
|
|
198
|
+
{
|
|
199
|
+
xmlns: "http://www.w3.org/2000/svg",
|
|
200
|
+
width: 24,
|
|
201
|
+
height: 24,
|
|
202
|
+
viewBox: "0 0 24 24",
|
|
203
|
+
fill: "none",
|
|
204
|
+
stroke: "currentColor",
|
|
205
|
+
strokeWidth: 2,
|
|
206
|
+
strokeLinecap: "round",
|
|
207
|
+
strokeLinejoin: "round",
|
|
208
|
+
"aria-hidden": "true",
|
|
209
|
+
className,
|
|
210
|
+
children: /* @__PURE__ */ jsx2("path", { d: "M21 12a9 9 0 1 1-6.219-8.56" })
|
|
211
|
+
}
|
|
212
|
+
);
|
|
213
|
+
}
|
|
214
|
+
function ConfirmDialog({
|
|
215
|
+
open,
|
|
216
|
+
onOpenChange,
|
|
217
|
+
title,
|
|
218
|
+
description,
|
|
219
|
+
content,
|
|
220
|
+
contentClassName,
|
|
221
|
+
confirmLabel = "Confirm",
|
|
222
|
+
cancelLabel = "Cancel",
|
|
223
|
+
variant = "default",
|
|
224
|
+
busy = false,
|
|
225
|
+
confirmDisabled = false,
|
|
226
|
+
onConfirm
|
|
227
|
+
}) {
|
|
228
|
+
return /* @__PURE__ */ jsx2(AlertDialog, { open, onOpenChange, children: /* @__PURE__ */ jsxs2(AlertDialogContent, { className: contentClassName, children: [
|
|
229
|
+
/* @__PURE__ */ jsxs2(AlertDialogHeader, { children: [
|
|
230
|
+
/* @__PURE__ */ jsx2(AlertDialogTitle, { children: title }),
|
|
231
|
+
description ? /* @__PURE__ */ jsx2(AlertDialogDescription, { children: description }) : null
|
|
232
|
+
] }),
|
|
233
|
+
content ?? null,
|
|
234
|
+
/* @__PURE__ */ jsxs2(AlertDialogFooter, { children: [
|
|
235
|
+
cancelLabel === null ? null : /* @__PURE__ */ jsx2(AlertDialogCancel, { className: "max-lg:min-h-11", disabled: busy, children: cancelLabel }),
|
|
236
|
+
/* @__PURE__ */ jsxs2(
|
|
237
|
+
AlertDialogAction,
|
|
238
|
+
{
|
|
239
|
+
disabled: busy || confirmDisabled,
|
|
240
|
+
onClick: (event) => {
|
|
241
|
+
event.preventDefault();
|
|
242
|
+
void onConfirm();
|
|
243
|
+
},
|
|
244
|
+
className: cn(
|
|
245
|
+
"max-lg:min-h-11",
|
|
246
|
+
variant === "destructive" && "bg-destructive text-destructive-foreground hover:bg-destructive/90"
|
|
247
|
+
),
|
|
248
|
+
children: [
|
|
249
|
+
busy ? /* @__PURE__ */ jsx2(SpinnerIcon, { className: "mr-2 h-4 w-4 animate-spin" }) : null,
|
|
250
|
+
confirmLabel
|
|
251
|
+
]
|
|
252
|
+
}
|
|
253
|
+
)
|
|
254
|
+
] })
|
|
255
|
+
] }) });
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
// src/confirm/host.tsx
|
|
259
|
+
import { jsx as jsx3 } from "react/jsx-runtime";
|
|
260
|
+
function ConfirmDialogHost() {
|
|
261
|
+
const [active, setActive] = React3.useState(null);
|
|
262
|
+
const [tick, setTick] = React3.useState(0);
|
|
263
|
+
const queueRef = React3.useRef([]);
|
|
264
|
+
React3.useEffect(() => {
|
|
265
|
+
const controller = {
|
|
266
|
+
show: (opts, resolve) => {
|
|
267
|
+
queueRef.current.push({ opts, resolve });
|
|
268
|
+
setTick((n) => n + 1);
|
|
269
|
+
}
|
|
270
|
+
};
|
|
271
|
+
_registerHost(controller);
|
|
272
|
+
return () => _unregisterHost(controller);
|
|
273
|
+
}, []);
|
|
274
|
+
React3.useEffect(() => {
|
|
275
|
+
if (active === null && queueRef.current.length > 0) {
|
|
276
|
+
setActive(queueRef.current.shift());
|
|
277
|
+
}
|
|
278
|
+
}, [active, tick]);
|
|
279
|
+
const handleConfirm = React3.useCallback(() => {
|
|
280
|
+
if (!active) return;
|
|
281
|
+
active.resolve(true);
|
|
282
|
+
setActive(null);
|
|
283
|
+
}, [active]);
|
|
284
|
+
const handleOpenChange = React3.useCallback(
|
|
285
|
+
(open) => {
|
|
286
|
+
if (!open && active) {
|
|
287
|
+
active.resolve(false);
|
|
288
|
+
setActive(null);
|
|
289
|
+
}
|
|
290
|
+
},
|
|
291
|
+
[active]
|
|
292
|
+
);
|
|
293
|
+
return /* @__PURE__ */ jsx3(
|
|
294
|
+
ConfirmDialog,
|
|
295
|
+
{
|
|
296
|
+
open: !!active,
|
|
297
|
+
onOpenChange: handleOpenChange,
|
|
298
|
+
title: active?.opts.title ?? "",
|
|
299
|
+
description: active?.opts.description,
|
|
300
|
+
confirmLabel: active?.opts.confirmLabel,
|
|
301
|
+
cancelLabel: active?.opts.cancelLabel,
|
|
302
|
+
variant: active?.opts.variant,
|
|
303
|
+
onConfirm: handleConfirm
|
|
304
|
+
}
|
|
305
|
+
);
|
|
306
|
+
}
|
|
307
|
+
export {
|
|
308
|
+
ConfirmDialog,
|
|
309
|
+
ConfirmDialogHost,
|
|
310
|
+
confirm
|
|
311
|
+
};
|
|
312
|
+
//# sourceMappingURL=confirm.js.map
|