@carlonicora/nextjs-jsonapi 1.112.1 → 1.113.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/dist/{BlockNoteEditor-HNQNKCFP.js → BlockNoteEditor-DOAPXS7S.js} +9 -9
- package/dist/{BlockNoteEditor-HNQNKCFP.js.map → BlockNoteEditor-DOAPXS7S.js.map} +1 -1
- package/dist/{BlockNoteEditor-A5LNFWHW.mjs → BlockNoteEditor-F3HSL4LX.mjs} +2 -2
- package/dist/billing/index.js +299 -299
- package/dist/billing/index.mjs +1 -1
- package/dist/{chunk-LYLGU2BK.mjs → chunk-CI36BXIG.mjs} +2940 -2837
- package/dist/chunk-CI36BXIG.mjs.map +1 -0
- package/dist/{chunk-DRJUN4R6.js → chunk-UBFVV2DA.js} +203 -100
- package/dist/chunk-UBFVV2DA.js.map +1 -0
- package/dist/client/index.js +2 -2
- package/dist/client/index.mjs +1 -1
- package/dist/components/index.d.mts +33 -2
- package/dist/components/index.d.ts +33 -2
- package/dist/components/index.js +10 -2
- package/dist/components/index.js.map +1 -1
- package/dist/components/index.mjs +9 -1
- package/dist/contexts/index.js +2 -2
- package/dist/contexts/index.mjs +1 -1
- package/dist/features/help/index.js +30 -30
- package/dist/features/help/index.mjs +1 -1
- package/package.json +1 -1
- package/src/components/tables/ContentListTable.tsx +2 -1
- package/src/shadcnui/index.ts +3 -0
- package/src/shadcnui/ui/__tests__/confirm-dialog.test.tsx +46 -0
- package/src/shadcnui/ui/__tests__/empty-state.test.tsx +26 -0
- package/src/shadcnui/ui/__tests__/spinner.test.tsx +18 -0
- package/src/shadcnui/ui/confirm-dialog.tsx +79 -0
- package/src/shadcnui/ui/empty-state.tsx +32 -0
- package/src/shadcnui/ui/spinner.tsx +36 -0
- package/dist/chunk-DRJUN4R6.js.map +0 -1
- package/dist/chunk-LYLGU2BK.mjs.map +0 -1
- /package/dist/{BlockNoteEditor-A5LNFWHW.mjs.map → BlockNoteEditor-F3HSL4LX.mjs.map} +0 -0
|
@@ -21,7 +21,7 @@ var _chunkYBZVWLPHjs = require('../../chunk-YBZVWLPH.js');
|
|
|
21
21
|
|
|
22
22
|
|
|
23
23
|
|
|
24
|
-
var
|
|
24
|
+
var _chunkUBFVV2DAjs = require('../../chunk-UBFVV2DA.js');
|
|
25
25
|
|
|
26
26
|
|
|
27
27
|
var _chunk6XWW67F2js = require('../../chunk-6XWW67F2.js');
|
|
@@ -73,29 +73,29 @@ var _lucidereact = require('lucide-react');
|
|
|
73
73
|
|
|
74
74
|
|
|
75
75
|
function HelpAssistantSheet({ open, onOpenChange }) {
|
|
76
|
-
return /* @__PURE__ */ _jsxruntime.jsx.call(void 0,
|
|
77
|
-
|
|
76
|
+
return /* @__PURE__ */ _jsxruntime.jsx.call(void 0, _chunkUBFVV2DAjs.Sheet, { open, onOpenChange, children: /* @__PURE__ */ _jsxruntime.jsx.call(void 0,
|
|
77
|
+
_chunkUBFVV2DAjs.SheetContent,
|
|
78
78
|
{
|
|
79
79
|
side: "right",
|
|
80
80
|
className: "flex w-full flex-col data-[side=right]:sm:max-w-2xl data-[side=right]:lg:max-w-3xl",
|
|
81
|
-
children: /* @__PURE__ */ _jsxruntime.jsx.call(void 0,
|
|
81
|
+
children: /* @__PURE__ */ _jsxruntime.jsx.call(void 0, _chunkUBFVV2DAjs.AssistantProvider, { manageUrl: false, children: /* @__PURE__ */ _jsxruntime.jsx.call(void 0, HelpAssistantSheetBody, {}) })
|
|
82
82
|
}
|
|
83
83
|
) });
|
|
84
84
|
}
|
|
85
85
|
_chunk7QVYU63Ejs.__name.call(void 0, HelpAssistantSheet, "HelpAssistantSheet");
|
|
86
86
|
function HelpAssistantSheetBody() {
|
|
87
87
|
const t = _nextintl.useTranslations.call(void 0, );
|
|
88
|
-
const ctx =
|
|
88
|
+
const ctx = _chunkUBFVV2DAjs.useAssistantContext.call(void 0, );
|
|
89
89
|
const showThread = !!ctx.assistant || ctx.sending || ctx.messages.length > 0;
|
|
90
90
|
const send = /* @__PURE__ */ _chunk7QVYU63Ejs.__name.call(void 0, (content) => ctx.sendMessage(content, { howToMode: true }), "send");
|
|
91
91
|
return /* @__PURE__ */ _jsxruntime.jsxs.call(void 0, _jsxruntime.Fragment, { children: [
|
|
92
|
-
/* @__PURE__ */ _jsxruntime.jsxs.call(void 0,
|
|
93
|
-
/* @__PURE__ */ _jsxruntime.jsx.call(void 0,
|
|
94
|
-
/* @__PURE__ */ _jsxruntime.jsx.call(void 0,
|
|
92
|
+
/* @__PURE__ */ _jsxruntime.jsxs.call(void 0, _chunkUBFVV2DAjs.SheetHeader, { children: [
|
|
93
|
+
/* @__PURE__ */ _jsxruntime.jsx.call(void 0, _chunkUBFVV2DAjs.SheetTitle, { children: t("help.askAi.sheet.title") }),
|
|
94
|
+
/* @__PURE__ */ _jsxruntime.jsx.call(void 0, _chunkUBFVV2DAjs.SheetDescription, { children: t("help.askAi.sheet.subtitle") })
|
|
95
95
|
] }),
|
|
96
|
-
!showThread ? /* @__PURE__ */ _jsxruntime.jsx.call(void 0,
|
|
96
|
+
!showThread ? /* @__PURE__ */ _jsxruntime.jsx.call(void 0, _chunkUBFVV2DAjs.AssistantEmptyState, { onSend: send }) : /* @__PURE__ */ _jsxruntime.jsxs.call(void 0, _jsxruntime.Fragment, { children: [
|
|
97
97
|
/* @__PURE__ */ _jsxruntime.jsx.call(void 0,
|
|
98
|
-
|
|
98
|
+
_chunkUBFVV2DAjs.AssistantThread,
|
|
99
99
|
{
|
|
100
100
|
messages: ctx.messages,
|
|
101
101
|
sending: ctx.sending,
|
|
@@ -105,7 +105,7 @@ function HelpAssistantSheetBody() {
|
|
|
105
105
|
onRetry: ctx.retrySend
|
|
106
106
|
}
|
|
107
107
|
),
|
|
108
|
-
/* @__PURE__ */ _jsxruntime.jsx.call(void 0,
|
|
108
|
+
/* @__PURE__ */ _jsxruntime.jsx.call(void 0, _chunkUBFVV2DAjs.AssistantComposer, { onSend: send, disabled: ctx.sending })
|
|
109
109
|
] })
|
|
110
110
|
] });
|
|
111
111
|
}
|
|
@@ -115,20 +115,20 @@ _chunk7QVYU63Ejs.__name.call(void 0, HelpAssistantSheetBody, "HelpAssistantSheet
|
|
|
115
115
|
|
|
116
116
|
function HelpAskAi() {
|
|
117
117
|
const t = _nextintl.useTranslations.call(void 0, );
|
|
118
|
-
const { currentUser } =
|
|
118
|
+
const { currentUser } = _chunkUBFVV2DAjs.useCurrentUserContext.call(void 0, );
|
|
119
119
|
const [open, setOpen] = _react.useState.call(void 0, false);
|
|
120
120
|
const disabled = !currentUser;
|
|
121
121
|
if (disabled) {
|
|
122
|
-
return /* @__PURE__ */ _jsxruntime.jsxs.call(void 0,
|
|
123
|
-
/* @__PURE__ */ _jsxruntime.jsxs.call(void 0,
|
|
122
|
+
return /* @__PURE__ */ _jsxruntime.jsxs.call(void 0, _chunkUBFVV2DAjs.Tooltip, { children: [
|
|
123
|
+
/* @__PURE__ */ _jsxruntime.jsxs.call(void 0, _chunkUBFVV2DAjs.TooltipTrigger, { render: /* @__PURE__ */ _jsxruntime.jsx.call(void 0, _chunkUBFVV2DAjs.Button, { variant: "outline", size: "sm", disabled: true }), children: [
|
|
124
124
|
/* @__PURE__ */ _jsxruntime.jsx.call(void 0, _lucidereact.SparklesIcon, { className: "h-4 w-4" }),
|
|
125
125
|
t("help.askAi.button")
|
|
126
126
|
] }),
|
|
127
|
-
/* @__PURE__ */ _jsxruntime.jsx.call(void 0,
|
|
127
|
+
/* @__PURE__ */ _jsxruntime.jsx.call(void 0, _chunkUBFVV2DAjs.TooltipContent, { children: t("help.askAi.loginTooltip") })
|
|
128
128
|
] });
|
|
129
129
|
}
|
|
130
130
|
return /* @__PURE__ */ _jsxruntime.jsxs.call(void 0, _jsxruntime.Fragment, { children: [
|
|
131
|
-
/* @__PURE__ */ _jsxruntime.jsxs.call(void 0,
|
|
131
|
+
/* @__PURE__ */ _jsxruntime.jsxs.call(void 0, _chunkUBFVV2DAjs.Button, { variant: "outline", size: "sm", onClick: () => setOpen(true), children: [
|
|
132
132
|
/* @__PURE__ */ _jsxruntime.jsx.call(void 0, _lucidereact.SparklesIcon, { className: "h-4 w-4" }),
|
|
133
133
|
t("help.askAi.button")
|
|
134
134
|
] }),
|
|
@@ -141,7 +141,7 @@ _chunk7QVYU63Ejs.__name.call(void 0, HelpAskAi, "HelpAskAi");
|
|
|
141
141
|
|
|
142
142
|
function HelpHeader() {
|
|
143
143
|
const t = _nextintl.useTranslations.call(void 0, );
|
|
144
|
-
const { currentUser } =
|
|
144
|
+
const { currentUser } = _chunkUBFVV2DAjs.useCurrentUserContext.call(void 0, );
|
|
145
145
|
const { brand } = useHelp();
|
|
146
146
|
const logo = _optionalChain([brand, 'optionalAccess', _ => _.logo]);
|
|
147
147
|
const label = _nullishCoalesce(_optionalChain([brand, 'optionalAccess', _2 => _2.label]), () => ( "Help"));
|
|
@@ -157,7 +157,7 @@ function HelpHeader() {
|
|
|
157
157
|
] }),
|
|
158
158
|
/* @__PURE__ */ _jsxruntime.jsxs.call(void 0, "div", { className: "flex items-center gap-2", children: [
|
|
159
159
|
/* @__PURE__ */ _jsxruntime.jsx.call(void 0, HelpAskAi, {}),
|
|
160
|
-
currentUser ? /* @__PURE__ */ _jsxruntime.jsx.call(void 0,
|
|
160
|
+
currentUser ? /* @__PURE__ */ _jsxruntime.jsx.call(void 0, _chunkUBFVV2DAjs.Button, { render: /* @__PURE__ */ _jsxruntime.jsx.call(void 0, _link2.default, { href: appHref }), nativeButton: false, variant: "outline", size: "sm", children: t("help.header.openApp") }) : /* @__PURE__ */ _jsxruntime.jsx.call(void 0, _chunkUBFVV2DAjs.Button, { render: /* @__PURE__ */ _jsxruntime.jsx.call(void 0, _link2.default, { href: "/login" }), nativeButton: false, variant: "outline", size: "sm", children: t("help.header.login") })
|
|
161
161
|
] })
|
|
162
162
|
] });
|
|
163
163
|
}
|
|
@@ -199,7 +199,7 @@ _chunk7QVYU63Ejs.__name.call(void 0, useHelpFilter, "useHelpFilter");
|
|
|
199
199
|
|
|
200
200
|
function HelpSideNav() {
|
|
201
201
|
const t = _nextintl.useTranslations.call(void 0, );
|
|
202
|
-
const generateUrl =
|
|
202
|
+
const generateUrl = _chunkUBFVV2DAjs.usePageUrlGenerator.call(void 0, );
|
|
203
203
|
const pathname = _navigation.usePathname.call(void 0, );
|
|
204
204
|
const [articles, setArticles] = _react.useState.call(void 0, []);
|
|
205
205
|
_react.useEffect.call(void 0, () => {
|
|
@@ -280,7 +280,7 @@ _chunk7QVYU63Ejs.__name.call(void 0, HelpSideNav, "HelpSideNav");
|
|
|
280
280
|
function HelpArticleBody(props) {
|
|
281
281
|
const t = _nextintl.useTranslations.call(void 0, );
|
|
282
282
|
const format = _nextintl.useFormatter.call(void 0, );
|
|
283
|
-
const generateUrl =
|
|
283
|
+
const generateUrl = _chunkUBFVV2DAjs.usePageUrlGenerator.call(void 0, );
|
|
284
284
|
const { howToType, title, summary, updatedAt, prev, next, children } = props;
|
|
285
285
|
return /* @__PURE__ */ _jsxruntime.jsxs.call(void 0, "article", { className: "prose dark:prose-invert max-w-none", children: [
|
|
286
286
|
/* @__PURE__ */ _jsxruntime.jsxs.call(void 0, "nav", { className: "text-muted-foreground mb-2 text-xs", children: [
|
|
@@ -358,7 +358,7 @@ _chunk7QVYU63Ejs.__name.call(void 0, HelpTOC, "HelpTOC");
|
|
|
358
358
|
|
|
359
359
|
function HelpHint({ contextKey }) {
|
|
360
360
|
const t = _nextintl.useTranslations.call(void 0, );
|
|
361
|
-
const generateUrl =
|
|
361
|
+
const generateUrl = _chunkUBFVV2DAjs.usePageUrlGenerator.call(void 0, );
|
|
362
362
|
const [open, setOpen] = _react.useState.call(void 0, false);
|
|
363
363
|
const [picked, setPicked] = _react.useState.call(void 0, null);
|
|
364
364
|
const [articles, setArticles] = _react.useState.call(void 0, []);
|
|
@@ -376,7 +376,7 @@ function HelpHint({ contextKey }) {
|
|
|
376
376
|
if (matches.length === 0) return null;
|
|
377
377
|
const active = _nullishCoalesce(picked, () => ( (matches.length === 1 ? matches[0] : null)));
|
|
378
378
|
return /* @__PURE__ */ _jsxruntime.jsxs.call(void 0,
|
|
379
|
-
|
|
379
|
+
_chunkUBFVV2DAjs.Sheet,
|
|
380
380
|
{
|
|
381
381
|
open,
|
|
382
382
|
onOpenChange: (o) => {
|
|
@@ -384,11 +384,11 @@ function HelpHint({ contextKey }) {
|
|
|
384
384
|
if (!o) setPicked(null);
|
|
385
385
|
},
|
|
386
386
|
children: [
|
|
387
|
-
/* @__PURE__ */ _jsxruntime.jsx.call(void 0,
|
|
388
|
-
/* @__PURE__ */ _jsxruntime.jsxs.call(void 0,
|
|
389
|
-
/* @__PURE__ */ _jsxruntime.jsxs.call(void 0,
|
|
390
|
-
/* @__PURE__ */ _jsxruntime.jsx.call(void 0,
|
|
391
|
-
/* @__PURE__ */ _jsxruntime.jsx.call(void 0,
|
|
387
|
+
/* @__PURE__ */ _jsxruntime.jsx.call(void 0, _chunkUBFVV2DAjs.SheetTrigger, { render: /* @__PURE__ */ _jsxruntime.jsx.call(void 0, _chunkUBFVV2DAjs.Button, { variant: "ghost", size: "icon-sm", "aria-label": t("help.hint.trigger") }), children: /* @__PURE__ */ _jsxruntime.jsx.call(void 0, _lucidereact.HelpCircleIcon, { className: "h-4 w-4" }) }),
|
|
388
|
+
/* @__PURE__ */ _jsxruntime.jsxs.call(void 0, _chunkUBFVV2DAjs.SheetContent, { side: "right", className: "w-full max-w-md sm:max-w-lg", children: [
|
|
389
|
+
/* @__PURE__ */ _jsxruntime.jsxs.call(void 0, _chunkUBFVV2DAjs.SheetHeader, { children: [
|
|
390
|
+
/* @__PURE__ */ _jsxruntime.jsx.call(void 0, _chunkUBFVV2DAjs.SheetTitle, { children: active ? active.name : t("help.hint.pickArticle") }),
|
|
391
|
+
/* @__PURE__ */ _jsxruntime.jsx.call(void 0, _chunkUBFVV2DAjs.SheetDescription, { children: _nullishCoalesce(_optionalChain([active, 'optionalAccess', _4 => _4.summary]), () => ( "")) })
|
|
392
392
|
] }),
|
|
393
393
|
active ? /* @__PURE__ */ _jsxruntime.jsxs.call(void 0, "div", { className: "mt-4 space-y-3", children: [
|
|
394
394
|
/* @__PURE__ */ _jsxruntime.jsx.call(void 0, "p", { className: "text-muted-foreground text-sm", children: active.summary }),
|
|
@@ -420,7 +420,7 @@ _chunk7QVYU63Ejs.__name.call(void 0, HelpHint, "HelpHint");
|
|
|
420
420
|
|
|
421
421
|
function HelpSearchResultRow({ result, onSelect }) {
|
|
422
422
|
const t = _nextintl.useTranslations.call(void 0, );
|
|
423
|
-
const generateUrl =
|
|
423
|
+
const generateUrl = _chunkUBFVV2DAjs.usePageUrlGenerator.call(void 0, );
|
|
424
424
|
const [article, setArticle] = _react.useState.call(void 0, null);
|
|
425
425
|
_react.useEffect.call(void 0, () => {
|
|
426
426
|
let active = true;
|
|
@@ -431,7 +431,7 @@ function HelpSearchResultRow({ result, onSelect }) {
|
|
|
431
431
|
}, [result.id]);
|
|
432
432
|
if (!article) {
|
|
433
433
|
return /* @__PURE__ */ _jsxruntime.jsx.call(void 0,
|
|
434
|
-
|
|
434
|
+
_chunkUBFVV2DAjs.CommandItem,
|
|
435
435
|
{
|
|
436
436
|
value: result.id,
|
|
437
437
|
disabled: true,
|
|
@@ -440,7 +440,7 @@ function HelpSearchResultRow({ result, onSelect }) {
|
|
|
440
440
|
}
|
|
441
441
|
);
|
|
442
442
|
}
|
|
443
|
-
return /* @__PURE__ */ _jsxruntime.jsx.call(void 0,
|
|
443
|
+
return /* @__PURE__ */ _jsxruntime.jsx.call(void 0, _chunkUBFVV2DAjs.CommandItem, { value: result.id, onSelect, className: "cursor-pointer p-0", children: /* @__PURE__ */ _jsxruntime.jsxs.call(void 0,
|
|
444
444
|
_link2.default,
|
|
445
445
|
{
|
|
446
446
|
href: generateUrl({ page: `/help/${article.howToType}/${article.slug}` }),
|
package/package.json
CHANGED
|
@@ -47,6 +47,7 @@ type ContentListTableProps = {
|
|
|
47
47
|
defaultExpanded?: boolean | ExpandedState;
|
|
48
48
|
fullWidth?: boolean;
|
|
49
49
|
groupBy?: string;
|
|
50
|
+
emptyState?: ReactNode;
|
|
50
51
|
};
|
|
51
52
|
|
|
52
53
|
export const ContentListTable = memo(function ContentListTable(props: ContentListTableProps) {
|
|
@@ -219,7 +220,7 @@ export const ContentListTable = memo(function ContentListTable(props: ContentLis
|
|
|
219
220
|
) : (
|
|
220
221
|
<TableRow>
|
|
221
222
|
<TableCell colSpan={tableColumns.length} className="h-24 text-center">
|
|
222
|
-
No results.
|
|
223
|
+
{props.emptyState ?? "No results."}
|
|
223
224
|
</TableCell>
|
|
224
225
|
</TableRow>
|
|
225
226
|
)}
|
package/src/shadcnui/index.ts
CHANGED
|
@@ -16,10 +16,12 @@ export * from "./ui/checkbox";
|
|
|
16
16
|
export * from "./ui/collapsible";
|
|
17
17
|
export * from "./ui/combobox";
|
|
18
18
|
export * from "./ui/command";
|
|
19
|
+
export * from "./ui/confirm-dialog";
|
|
19
20
|
export * from "./ui/context-menu";
|
|
20
21
|
export * from "./ui/dialog";
|
|
21
22
|
export * from "./ui/drawer";
|
|
22
23
|
export * from "./ui/dropdown-menu";
|
|
24
|
+
export * from "./ui/empty-state";
|
|
23
25
|
export * from "./ui/field";
|
|
24
26
|
export * from "./ui/form";
|
|
25
27
|
export * from "./ui/hover-card";
|
|
@@ -40,6 +42,7 @@ export * from "./ui/sidebar";
|
|
|
40
42
|
export * from "./ui/skeleton";
|
|
41
43
|
export * from "./ui/slider";
|
|
42
44
|
export * from "./ui/sonner";
|
|
45
|
+
export * from "./ui/spinner";
|
|
43
46
|
export * from "./ui/switch";
|
|
44
47
|
export * from "./ui/table";
|
|
45
48
|
export * from "./ui/tabs";
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
// packages/nextjs-jsonapi/src/shadcnui/ui/__tests__/confirm-dialog.test.tsx
|
|
2
|
+
import { describe, expect, it, vi } from "vitest";
|
|
3
|
+
import { render, screen } from "@testing-library/react";
|
|
4
|
+
import userEvent from "@testing-library/user-event";
|
|
5
|
+
import { ConfirmDialog } from "../confirm-dialog";
|
|
6
|
+
|
|
7
|
+
describe("ConfirmDialog", () => {
|
|
8
|
+
it("renders title/description and calls onConfirm then closes", async () => {
|
|
9
|
+
const onConfirm = vi.fn();
|
|
10
|
+
const onOpenChange = vi.fn();
|
|
11
|
+
render(
|
|
12
|
+
<ConfirmDialog
|
|
13
|
+
open
|
|
14
|
+
onOpenChange={onOpenChange}
|
|
15
|
+
title="Reveal plot secrets?"
|
|
16
|
+
description="Spoilers ahead."
|
|
17
|
+
confirmLabel="Reveal"
|
|
18
|
+
cancelLabel="Keep hidden"
|
|
19
|
+
destructive
|
|
20
|
+
onConfirm={onConfirm}
|
|
21
|
+
/>,
|
|
22
|
+
);
|
|
23
|
+
expect(screen.getByText("Reveal plot secrets?")).toBeInTheDocument();
|
|
24
|
+
expect(screen.getByText("Spoilers ahead.")).toBeInTheDocument();
|
|
25
|
+
await userEvent.click(screen.getByRole("button", { name: "Reveal" }));
|
|
26
|
+
expect(onConfirm).toHaveBeenCalledTimes(1);
|
|
27
|
+
expect(onOpenChange).toHaveBeenCalledWith(false);
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it("cancel closes without confirming", async () => {
|
|
31
|
+
const onConfirm = vi.fn();
|
|
32
|
+
const onOpenChange = vi.fn();
|
|
33
|
+
render(
|
|
34
|
+
<ConfirmDialog
|
|
35
|
+
open
|
|
36
|
+
onOpenChange={onOpenChange}
|
|
37
|
+
title="t"
|
|
38
|
+
confirmLabel="Yes"
|
|
39
|
+
cancelLabel="No"
|
|
40
|
+
onConfirm={onConfirm}
|
|
41
|
+
/>,
|
|
42
|
+
);
|
|
43
|
+
await userEvent.click(screen.getByRole("button", { name: "No" }));
|
|
44
|
+
expect(onConfirm).not.toHaveBeenCalled();
|
|
45
|
+
});
|
|
46
|
+
});
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
// packages/nextjs-jsonapi/src/shadcnui/ui/__tests__/empty-state.test.tsx
|
|
2
|
+
import { describe, expect, it } from "vitest";
|
|
3
|
+
import { render, screen } from "@testing-library/react";
|
|
4
|
+
import { InboxIcon } from "lucide-react";
|
|
5
|
+
import { EmptyState } from "../empty-state";
|
|
6
|
+
|
|
7
|
+
describe("EmptyState", () => {
|
|
8
|
+
it("renders title, description and action", () => {
|
|
9
|
+
render(
|
|
10
|
+
<EmptyState
|
|
11
|
+
icon={InboxIcon}
|
|
12
|
+
title="No games yet"
|
|
13
|
+
description="Create your first game."
|
|
14
|
+
action={<button type="button">New game</button>}
|
|
15
|
+
/>,
|
|
16
|
+
);
|
|
17
|
+
expect(screen.getByText("No games yet")).toBeInTheDocument();
|
|
18
|
+
expect(screen.getByText("Create your first game.")).toBeInTheDocument();
|
|
19
|
+
expect(screen.getByRole("button", { name: "New game" })).toBeInTheDocument();
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
it("renders without optional parts", () => {
|
|
23
|
+
render(<EmptyState title="Empty" />);
|
|
24
|
+
expect(screen.getByText("Empty")).toBeInTheDocument();
|
|
25
|
+
});
|
|
26
|
+
});
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
// packages/nextjs-jsonapi/src/shadcnui/ui/__tests__/spinner.test.tsx
|
|
2
|
+
import { describe, expect, it } from "vitest";
|
|
3
|
+
import { render, screen } from "@testing-library/react";
|
|
4
|
+
import { Spinner } from "../spinner";
|
|
5
|
+
|
|
6
|
+
describe("Spinner", () => {
|
|
7
|
+
it("renders a status indicator with animate-spin", () => {
|
|
8
|
+
render(<Spinner />);
|
|
9
|
+
const spinner = screen.getByRole("status");
|
|
10
|
+
expect(spinner).toHaveClass("animate-spin");
|
|
11
|
+
expect(spinner).toHaveClass("size-4");
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
it("applies the size variant", () => {
|
|
15
|
+
render(<Spinner size="lg" />);
|
|
16
|
+
expect(screen.getByRole("status")).toHaveClass("size-8");
|
|
17
|
+
});
|
|
18
|
+
});
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import * as React from "react";
|
|
4
|
+
|
|
5
|
+
import {
|
|
6
|
+
AlertDialog,
|
|
7
|
+
AlertDialogCancel,
|
|
8
|
+
AlertDialogContent,
|
|
9
|
+
AlertDialogDescription,
|
|
10
|
+
AlertDialogFooter,
|
|
11
|
+
AlertDialogHeader,
|
|
12
|
+
AlertDialogTitle,
|
|
13
|
+
} from "@/components/ui/alert-dialog";
|
|
14
|
+
import { Button } from "@/components/ui/button";
|
|
15
|
+
|
|
16
|
+
type ConfirmDialogProps = {
|
|
17
|
+
open: boolean;
|
|
18
|
+
onOpenChange: (open: boolean) => void;
|
|
19
|
+
title: string;
|
|
20
|
+
description?: React.ReactNode;
|
|
21
|
+
confirmLabel: string;
|
|
22
|
+
cancelLabel: string;
|
|
23
|
+
destructive?: boolean;
|
|
24
|
+
onConfirm: () => void | Promise<void>;
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Convenience confirmation dialog. Controlled-only and trigger-less by design:
|
|
29
|
+
* callers render their own trigger Button and own the open state, which avoids
|
|
30
|
+
* every trigger-composition pitfall (no asChild, no nested <button>).
|
|
31
|
+
*/
|
|
32
|
+
function ConfirmDialog({
|
|
33
|
+
open,
|
|
34
|
+
onOpenChange,
|
|
35
|
+
title,
|
|
36
|
+
description,
|
|
37
|
+
confirmLabel,
|
|
38
|
+
cancelLabel,
|
|
39
|
+
destructive = false,
|
|
40
|
+
onConfirm,
|
|
41
|
+
}: ConfirmDialogProps) {
|
|
42
|
+
const [pending, setPending] = React.useState(false);
|
|
43
|
+
|
|
44
|
+
const handleConfirm = async () => {
|
|
45
|
+
try {
|
|
46
|
+
setPending(true);
|
|
47
|
+
await onConfirm();
|
|
48
|
+
onOpenChange(false);
|
|
49
|
+
} catch {
|
|
50
|
+
// A rejecting onConfirm keeps the dialog open; the caller owns user-facing
|
|
51
|
+
// error feedback inside onConfirm.
|
|
52
|
+
} finally {
|
|
53
|
+
setPending(false);
|
|
54
|
+
}
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
return (
|
|
58
|
+
<AlertDialog open={open} onOpenChange={(next) => !pending && onOpenChange(next)}>
|
|
59
|
+
<AlertDialogContent>
|
|
60
|
+
<AlertDialogHeader>
|
|
61
|
+
<AlertDialogTitle>{title}</AlertDialogTitle>
|
|
62
|
+
{description && <AlertDialogDescription>{description}</AlertDialogDescription>}
|
|
63
|
+
</AlertDialogHeader>
|
|
64
|
+
<AlertDialogFooter>
|
|
65
|
+
<AlertDialogCancel disabled={pending}>{cancelLabel}</AlertDialogCancel>
|
|
66
|
+
<Button
|
|
67
|
+
variant={destructive ? "destructive" : "default"}
|
|
68
|
+
onClick={() => void handleConfirm()}
|
|
69
|
+
disabled={pending}
|
|
70
|
+
>
|
|
71
|
+
{confirmLabel}
|
|
72
|
+
</Button>
|
|
73
|
+
</AlertDialogFooter>
|
|
74
|
+
</AlertDialogContent>
|
|
75
|
+
</AlertDialog>
|
|
76
|
+
);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export { ConfirmDialog, type ConfirmDialogProps };
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import type { LucideIcon } from "lucide-react";
|
|
2
|
+
import * as React from "react";
|
|
3
|
+
|
|
4
|
+
import { cn } from "@/lib/utils";
|
|
5
|
+
|
|
6
|
+
type EmptyStateProps = React.ComponentProps<"div"> & {
|
|
7
|
+
icon?: LucideIcon;
|
|
8
|
+
title: string;
|
|
9
|
+
description?: string;
|
|
10
|
+
action?: React.ReactNode;
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
function EmptyState({ icon: Icon, title, description, action, className, ...props }: EmptyStateProps) {
|
|
14
|
+
return (
|
|
15
|
+
<div
|
|
16
|
+
data-slot="empty-state"
|
|
17
|
+
className={cn("flex flex-col items-center justify-center gap-1.5 rounded-md p-6 text-center", className)}
|
|
18
|
+
{...props}
|
|
19
|
+
>
|
|
20
|
+
{Icon && (
|
|
21
|
+
<div className="bg-muted text-muted-foreground mb-1 inline-flex size-8 items-center justify-center rounded-md">
|
|
22
|
+
<Icon className="size-4" />
|
|
23
|
+
</div>
|
|
24
|
+
)}
|
|
25
|
+
<div className="text-sm font-medium">{title}</div>
|
|
26
|
+
{description && <div className="text-muted-foreground text-xs/relaxed">{description}</div>}
|
|
27
|
+
{action && <div className="mt-2">{action}</div>}
|
|
28
|
+
</div>
|
|
29
|
+
);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export { EmptyState };
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { cva, type VariantProps } from "class-variance-authority";
|
|
2
|
+
import { Loader2Icon } from "lucide-react";
|
|
3
|
+
import * as React from "react";
|
|
4
|
+
|
|
5
|
+
import { cn } from "@/lib/utils";
|
|
6
|
+
|
|
7
|
+
const spinnerVariants = cva("animate-spin", {
|
|
8
|
+
variants: {
|
|
9
|
+
size: {
|
|
10
|
+
sm: "size-3.5",
|
|
11
|
+
default: "size-4",
|
|
12
|
+
lg: "size-8",
|
|
13
|
+
},
|
|
14
|
+
},
|
|
15
|
+
defaultVariants: {
|
|
16
|
+
size: "default",
|
|
17
|
+
},
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
function Spinner({
|
|
21
|
+
className,
|
|
22
|
+
size,
|
|
23
|
+
...props
|
|
24
|
+
}: React.ComponentProps<typeof Loader2Icon> & VariantProps<typeof spinnerVariants>) {
|
|
25
|
+
return (
|
|
26
|
+
<Loader2Icon
|
|
27
|
+
data-slot="spinner"
|
|
28
|
+
role="status"
|
|
29
|
+
aria-label="Loading"
|
|
30
|
+
className={cn(spinnerVariants({ size, className }))}
|
|
31
|
+
{...props}
|
|
32
|
+
/>
|
|
33
|
+
);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export { Spinner, spinnerVariants };
|