@questpie/admin 0.0.1 → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (250) hide show
  1. package/README.md +439 -424
  2. package/dist/auth-layout-M8K8_q5R.mjs +181 -0
  3. package/dist/auth-layout-M8K8_q5R.mjs.map +1 -0
  4. package/dist/bulk-upload-dialog-h7zXD78Y.mjs +274 -0
  5. package/dist/bulk-upload-dialog-h7zXD78Y.mjs.map +1 -0
  6. package/dist/{components/ui/card.mjs → card-BKHjBQfw.mjs} +8 -8
  7. package/dist/card-BKHjBQfw.mjs.map +1 -0
  8. package/dist/client/styles/index.css +434 -0
  9. package/dist/client-BCGpkAz6.mjs +22635 -0
  10. package/dist/client-BCGpkAz6.mjs.map +1 -0
  11. package/dist/client-CcWZbkBP.d.mts +13585 -0
  12. package/dist/client-CcWZbkBP.d.mts.map +1 -0
  13. package/dist/client.d.mts +3 -0
  14. package/dist/client.mjs +14 -0
  15. package/dist/content-locales-provider-BXvuIgfg.mjs +1650 -0
  16. package/dist/content-locales-provider-BXvuIgfg.mjs.map +1 -0
  17. package/dist/dashboard-page-B4PGEdc2.mjs +2500 -0
  18. package/dist/dashboard-page-B4PGEdc2.mjs.map +1 -0
  19. package/dist/dashboard-page-CVlyR40m.mjs +6 -0
  20. package/dist/dropzone-Do3awXKd.mjs +634 -0
  21. package/dist/dropzone-Do3awXKd.mjs.map +1 -0
  22. package/dist/{views/auth/forgot-password-form.mjs → forgot-password-page-Bcp-An4Y.mjs} +87 -14
  23. package/dist/forgot-password-page-Bcp-An4Y.mjs.map +1 -0
  24. package/dist/forgot-password-page-CIILVhfo.mjs +7 -0
  25. package/dist/index-B9Xwk4hi.d.mts +2753 -0
  26. package/dist/index-B9Xwk4hi.d.mts.map +1 -0
  27. package/dist/index.d.mts +3 -0
  28. package/dist/index.mjs +14 -0
  29. package/dist/login-page-8K7fo0qK.mjs +7 -0
  30. package/dist/login-page-CP4gA-dl.mjs +298 -0
  31. package/dist/login-page-CP4gA-dl.mjs.map +1 -0
  32. package/dist/preview-utils-BKQ9-TMa.mjs +65 -0
  33. package/dist/preview-utils-BKQ9-TMa.mjs.map +1 -0
  34. package/dist/{views/auth/reset-password-form.mjs → reset-password-page-BqfDmLxA.mjs} +111 -14
  35. package/dist/reset-password-page-BqfDmLxA.mjs.map +1 -0
  36. package/dist/reset-password-page-DLATv0xQ.mjs +7 -0
  37. package/dist/runtime-6VZM878K.mjs +69 -0
  38. package/dist/runtime-6VZM878K.mjs.map +1 -0
  39. package/dist/saved-views.types-BMsz5mCy.d.mts +42 -0
  40. package/dist/saved-views.types-BMsz5mCy.d.mts.map +1 -0
  41. package/dist/server.d.mts +250 -0
  42. package/dist/server.d.mts.map +1 -0
  43. package/dist/server.mjs +832 -0
  44. package/dist/server.mjs.map +1 -0
  45. package/dist/setup-page-CMZ5P_OE.mjs +6 -0
  46. package/dist/setup-page-YAP_fzqh.mjs +264 -0
  47. package/dist/setup-page-YAP_fzqh.mjs.map +1 -0
  48. package/dist/shared.d.mts +57 -0
  49. package/dist/shared.d.mts.map +1 -0
  50. package/dist/shared.mjs +3 -0
  51. package/dist/{hooks/use-auth.mjs → use-auth-BoLmWtmU.mjs} +42 -30
  52. package/dist/use-auth-BoLmWtmU.mjs.map +1 -0
  53. package/package.json +48 -197
  54. package/.turbo/turbo-build.log +0 -108
  55. package/CHANGELOG.md +0 -10
  56. package/STATUS.md +0 -917
  57. package/VALIDATION.md +0 -602
  58. package/components.json +0 -24
  59. package/dist/__tests__/setup.mjs +0 -38
  60. package/dist/__tests__/test-utils.mjs +0 -45
  61. package/dist/__tests__/vitest.d.mjs +0 -3
  62. package/dist/components/admin-app.mjs +0 -69
  63. package/dist/components/fields/array-field.mjs +0 -190
  64. package/dist/components/fields/checkbox-field.mjs +0 -34
  65. package/dist/components/fields/custom-field.mjs +0 -32
  66. package/dist/components/fields/date-field.mjs +0 -41
  67. package/dist/components/fields/datetime-field.mjs +0 -42
  68. package/dist/components/fields/email-field.mjs +0 -37
  69. package/dist/components/fields/embedded-collection.mjs +0 -253
  70. package/dist/components/fields/field-types.mjs +0 -1
  71. package/dist/components/fields/field-utils.mjs +0 -10
  72. package/dist/components/fields/field-wrapper.mjs +0 -34
  73. package/dist/components/fields/index.mjs +0 -23
  74. package/dist/components/fields/json-field.mjs +0 -243
  75. package/dist/components/fields/locale-badge.mjs +0 -16
  76. package/dist/components/fields/number-field.mjs +0 -39
  77. package/dist/components/fields/password-field.mjs +0 -37
  78. package/dist/components/fields/relation-field.mjs +0 -104
  79. package/dist/components/fields/relation-picker.mjs +0 -229
  80. package/dist/components/fields/relation-select.mjs +0 -188
  81. package/dist/components/fields/rich-text-editor/index.mjs +0 -897
  82. package/dist/components/fields/select-field.mjs +0 -41
  83. package/dist/components/fields/switch-field.mjs +0 -34
  84. package/dist/components/fields/text-field.mjs +0 -38
  85. package/dist/components/fields/textarea-field.mjs +0 -38
  86. package/dist/components/index.mjs +0 -59
  87. package/dist/components/primitives/checkbox-input.mjs +0 -127
  88. package/dist/components/primitives/date-input.mjs +0 -303
  89. package/dist/components/primitives/index.mjs +0 -12
  90. package/dist/components/primitives/number-input.mjs +0 -104
  91. package/dist/components/primitives/select-input.mjs +0 -177
  92. package/dist/components/primitives/tag-input.mjs +0 -135
  93. package/dist/components/primitives/text-input.mjs +0 -39
  94. package/dist/components/primitives/textarea-input.mjs +0 -37
  95. package/dist/components/primitives/toggle-input.mjs +0 -31
  96. package/dist/components/primitives/types.mjs +0 -12
  97. package/dist/components/ui/accordion.mjs +0 -55
  98. package/dist/components/ui/avatar.mjs +0 -54
  99. package/dist/components/ui/badge.mjs +0 -34
  100. package/dist/components/ui/button.mjs +0 -48
  101. package/dist/components/ui/checkbox.mjs +0 -21
  102. package/dist/components/ui/combobox.mjs +0 -163
  103. package/dist/components/ui/dialog.mjs +0 -95
  104. package/dist/components/ui/dropdown-menu.mjs +0 -138
  105. package/dist/components/ui/field.mjs +0 -113
  106. package/dist/components/ui/input-group.mjs +0 -82
  107. package/dist/components/ui/input.mjs +0 -17
  108. package/dist/components/ui/label.mjs +0 -15
  109. package/dist/components/ui/popover.mjs +0 -56
  110. package/dist/components/ui/scroll-area.mjs +0 -38
  111. package/dist/components/ui/select.mjs +0 -100
  112. package/dist/components/ui/separator.mjs +0 -16
  113. package/dist/components/ui/sheet.mjs +0 -90
  114. package/dist/components/ui/sidebar.mjs +0 -387
  115. package/dist/components/ui/skeleton.mjs +0 -14
  116. package/dist/components/ui/spinner.mjs +0 -16
  117. package/dist/components/ui/switch.mjs +0 -22
  118. package/dist/components/ui/table.mjs +0 -68
  119. package/dist/components/ui/tabs.mjs +0 -48
  120. package/dist/components/ui/textarea.mjs +0 -15
  121. package/dist/components/ui/tooltip.mjs +0 -44
  122. package/dist/config/component-registry.mjs +0 -38
  123. package/dist/config/index.mjs +0 -129
  124. package/dist/hooks/admin-provider.mjs +0 -70
  125. package/dist/hooks/index.mjs +0 -7
  126. package/dist/hooks/store.mjs +0 -178
  127. package/dist/hooks/use-collection-db.mjs +0 -146
  128. package/dist/hooks/use-collection.mjs +0 -112
  129. package/dist/hooks/use-global.mjs +0 -46
  130. package/dist/hooks/use-mobile.mjs +0 -20
  131. package/dist/lib/utils.mjs +0 -10
  132. package/dist/styles/index.css +0 -336
  133. package/dist/styles/index.mjs +0 -1
  134. package/dist/utils/index.mjs +0 -9
  135. package/dist/views/auth/auth-layout.mjs +0 -52
  136. package/dist/views/auth/index.mjs +0 -6
  137. package/dist/views/auth/login-form.mjs +0 -156
  138. package/dist/views/collection/auto-form-fields.mjs +0 -525
  139. package/dist/views/collection/collection-form.mjs +0 -91
  140. package/dist/views/collection/collection-list.mjs +0 -76
  141. package/dist/views/collection/form-field.mjs +0 -42
  142. package/dist/views/collection/index.mjs +0 -6
  143. package/dist/views/common/index.mjs +0 -4
  144. package/dist/views/common/locale-switcher.mjs +0 -39
  145. package/dist/views/common/version-history.mjs +0 -272
  146. package/dist/views/index.mjs +0 -9
  147. package/dist/views/layout/admin-layout.mjs +0 -40
  148. package/dist/views/layout/admin-router.mjs +0 -95
  149. package/dist/views/layout/admin-sidebar.mjs +0 -63
  150. package/dist/views/layout/index.mjs +0 -5
  151. package/src/__tests__/setup.ts +0 -44
  152. package/src/__tests__/test-utils.tsx +0 -49
  153. package/src/__tests__/vitest.d.ts +0 -9
  154. package/src/components/admin-app.tsx +0 -221
  155. package/src/components/fields/array-field.tsx +0 -237
  156. package/src/components/fields/checkbox-field.tsx +0 -47
  157. package/src/components/fields/custom-field.tsx +0 -50
  158. package/src/components/fields/date-field.tsx +0 -65
  159. package/src/components/fields/datetime-field.tsx +0 -67
  160. package/src/components/fields/email-field.tsx +0 -51
  161. package/src/components/fields/embedded-collection.tsx +0 -315
  162. package/src/components/fields/field-types.ts +0 -162
  163. package/src/components/fields/field-utils.ts +0 -6
  164. package/src/components/fields/field-wrapper.tsx +0 -52
  165. package/src/components/fields/index.ts +0 -66
  166. package/src/components/fields/json-field.tsx +0 -440
  167. package/src/components/fields/locale-badge.tsx +0 -15
  168. package/src/components/fields/number-field.tsx +0 -57
  169. package/src/components/fields/password-field.tsx +0 -51
  170. package/src/components/fields/relation-field.tsx +0 -243
  171. package/src/components/fields/relation-picker.tsx +0 -402
  172. package/src/components/fields/relation-select.tsx +0 -327
  173. package/src/components/fields/rich-text-editor/index.tsx +0 -1337
  174. package/src/components/fields/select-field.tsx +0 -61
  175. package/src/components/fields/switch-field.tsx +0 -47
  176. package/src/components/fields/text-field.tsx +0 -55
  177. package/src/components/fields/textarea-field.tsx +0 -55
  178. package/src/components/index.ts +0 -40
  179. package/src/components/primitives/checkbox-input.tsx +0 -193
  180. package/src/components/primitives/date-input.tsx +0 -401
  181. package/src/components/primitives/index.ts +0 -24
  182. package/src/components/primitives/number-input.tsx +0 -132
  183. package/src/components/primitives/select-input.tsx +0 -296
  184. package/src/components/primitives/tag-input.tsx +0 -200
  185. package/src/components/primitives/text-input.tsx +0 -49
  186. package/src/components/primitives/textarea-input.tsx +0 -46
  187. package/src/components/primitives/toggle-input.tsx +0 -36
  188. package/src/components/primitives/types.ts +0 -235
  189. package/src/components/ui/accordion.tsx +0 -72
  190. package/src/components/ui/avatar.tsx +0 -106
  191. package/src/components/ui/badge.tsx +0 -48
  192. package/src/components/ui/button.tsx +0 -53
  193. package/src/components/ui/card.tsx +0 -94
  194. package/src/components/ui/checkbox.tsx +0 -27
  195. package/src/components/ui/combobox.tsx +0 -290
  196. package/src/components/ui/dialog.tsx +0 -151
  197. package/src/components/ui/dropdown-menu.tsx +0 -254
  198. package/src/components/ui/field.tsx +0 -227
  199. package/src/components/ui/input-group.tsx +0 -149
  200. package/src/components/ui/input.tsx +0 -20
  201. package/src/components/ui/label.tsx +0 -18
  202. package/src/components/ui/popover.tsx +0 -88
  203. package/src/components/ui/scroll-area.tsx +0 -53
  204. package/src/components/ui/select.tsx +0 -192
  205. package/src/components/ui/separator.tsx +0 -23
  206. package/src/components/ui/sheet.tsx +0 -127
  207. package/src/components/ui/sidebar.tsx +0 -723
  208. package/src/components/ui/skeleton.tsx +0 -13
  209. package/src/components/ui/spinner.tsx +0 -10
  210. package/src/components/ui/switch.tsx +0 -32
  211. package/src/components/ui/table.tsx +0 -99
  212. package/src/components/ui/tabs.tsx +0 -82
  213. package/src/components/ui/textarea.tsx +0 -18
  214. package/src/components/ui/tooltip.tsx +0 -70
  215. package/src/config/component-registry.ts +0 -190
  216. package/src/config/index.ts +0 -1099
  217. package/src/hooks/README.md +0 -269
  218. package/src/hooks/admin-provider.tsx +0 -110
  219. package/src/hooks/index.ts +0 -41
  220. package/src/hooks/store.ts +0 -248
  221. package/src/hooks/use-auth.ts +0 -168
  222. package/src/hooks/use-collection-db.ts +0 -209
  223. package/src/hooks/use-collection.ts +0 -156
  224. package/src/hooks/use-global.ts +0 -69
  225. package/src/hooks/use-mobile.ts +0 -21
  226. package/src/lib/utils.ts +0 -6
  227. package/src/styles/index.css +0 -340
  228. package/src/utils/index.ts +0 -6
  229. package/src/views/auth/auth-layout.tsx +0 -77
  230. package/src/views/auth/forgot-password-form.tsx +0 -192
  231. package/src/views/auth/index.ts +0 -21
  232. package/src/views/auth/login-form.tsx +0 -229
  233. package/src/views/auth/reset-password-form.tsx +0 -232
  234. package/src/views/collection/auto-form-fields.tsx +0 -982
  235. package/src/views/collection/collection-form.tsx +0 -186
  236. package/src/views/collection/collection-list.tsx +0 -223
  237. package/src/views/collection/form-field.tsx +0 -52
  238. package/src/views/collection/index.ts +0 -15
  239. package/src/views/common/index.ts +0 -8
  240. package/src/views/common/locale-switcher.tsx +0 -45
  241. package/src/views/common/version-history.tsx +0 -406
  242. package/src/views/index.ts +0 -25
  243. package/src/views/layout/admin-layout.tsx +0 -117
  244. package/src/views/layout/admin-router.tsx +0 -206
  245. package/src/views/layout/admin-sidebar.tsx +0 -185
  246. package/src/views/layout/index.ts +0 -12
  247. package/tsconfig.json +0 -13
  248. package/tsconfig.tsbuildinfo +0 -1
  249. package/tsdown.config.ts +0 -13
  250. package/vitest.config.ts +0 -29
@@ -0,0 +1,2500 @@
1
+ import { _ as formatCollectionName, a as selectBasePath, f as useAdminStore, g as cn, h as Button, l as selectNavigate, r as selectAdmin, s as selectClient, v as useResolveText } from "./content-locales-provider-BXvuIgfg.mjs";
2
+ import { r as useScopedLocale } from "./runtime-6VZM878K.mjs";
3
+ import { i as CardDescription, n as CardAction, o as CardHeader, r as CardContent, s as CardTitle, t as Card } from "./card-BKHjBQfw.mjs";
4
+ import { ArrowClockwise, ArrowRight, ArrowsOutSimple, CaretDownIcon, Circle } from "@phosphor-icons/react";
5
+ import * as React$1 from "react";
6
+ import { useMemo } from "react";
7
+ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
8
+ import { cva } from "class-variance-authority";
9
+ import { mergeProps } from "@base-ui/react/merge-props";
10
+ import { useRender } from "@base-ui/react/use-render";
11
+ import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
12
+ import { createQuestpieQueryOptions } from "@questpie/tanstack-query";
13
+ import { Accordion } from "@base-ui/react/accordion";
14
+ import { Tabs } from "@base-ui/react/tabs";
15
+ import { Area, AreaChart, Bar, BarChart, CartesianGrid, Cell, Line, LineChart, Pie, PieChart, ResponsiveContainer, Tooltip, XAxis, YAxis } from "recharts";
16
+
17
+ //#region src/client/components/ui/badge.tsx
18
+ const badgeVariants = cva("h-5 gap-1 rounded-none border border-transparent px-2 py-0.5 text-[0.625rem] font-medium transition-all has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 [&>svg]:size-2.5! inline-flex items-center justify-center w-fit whitespace-nowrap shrink-0 [&>svg]:pointer-events-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive transition-colors overflow-hidden group/badge", {
19
+ variants: { variant: {
20
+ default: "bg-primary/10 text-primary border-primary border backdrop-blur-sm [a]:hover:bg-primary/80",
21
+ secondary: "bg-secondary text-secondary-foreground [a]:hover:bg-secondary/80",
22
+ destructive: "bg-destructive/10 [a]:hover:bg-destructive/20 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 text-destructive dark:bg-destructive/20",
23
+ outline: "border-border text-foreground [a]:hover:bg-muted [a]:hover:text-muted-foreground bg-input/20 ",
24
+ ghost: "hover:bg-muted hover:text-muted-foreground dark:hover:bg-muted/50",
25
+ link: "text-primary underline-offset-4 hover:underline"
26
+ } },
27
+ defaultVariants: { variant: "default" }
28
+ });
29
+ function Badge({ className, variant = "default", render, ...props }) {
30
+ return useRender({
31
+ defaultTagName: "span",
32
+ props: mergeProps({ className: cn(badgeVariants({
33
+ className,
34
+ variant
35
+ })) }, props),
36
+ render,
37
+ state: {
38
+ slot: "badge",
39
+ variant
40
+ }
41
+ });
42
+ }
43
+
44
+ //#endregion
45
+ //#region src/client/views/collection/cells/primitive-cells.tsx
46
+ /**
47
+ * Ultra-simple default cell renderer
48
+ * Used as fallback when field has no .cell defined
49
+ */
50
+ function DefaultCell({ value }) {
51
+ if (value === null || value === void 0) return /* @__PURE__ */ jsx("span", {
52
+ className: "text-muted-foreground",
53
+ children: "-"
54
+ });
55
+ return /* @__PURE__ */ jsx("span", { children: String(value) });
56
+ }
57
+ /**
58
+ * Text cell - simple text display with truncation
59
+ */
60
+ function TextCell({ value }) {
61
+ if (value === null || value === void 0 || value === "") return /* @__PURE__ */ jsx("span", {
62
+ className: "text-muted-foreground",
63
+ children: "-"
64
+ });
65
+ const text = String(value);
66
+ return /* @__PURE__ */ jsx("span", {
67
+ className: "truncate max-w-[300px] block",
68
+ title: text,
69
+ children: text
70
+ });
71
+ }
72
+ function stripHtmlTags(value) {
73
+ return value.replace(/<[^>]*>/g, " ").replace(/\s+/g, " ").trim();
74
+ }
75
+ function extractTextFromNode(node) {
76
+ if (!node) return "";
77
+ if (Array.isArray(node)) return node.map(extractTextFromNode).filter(Boolean).join(" ");
78
+ if (typeof node.text === "string") return node.text;
79
+ if (Array.isArray(node.content)) return node.content.map(extractTextFromNode).filter(Boolean).join(" ");
80
+ return "";
81
+ }
82
+ function getRichTextPreview(value) {
83
+ if (value === null || value === void 0) return "";
84
+ if (typeof value === "string") return /<\/?[a-z][\s\S]*>/i.test(value) ? stripHtmlTags(value) : value.trim();
85
+ if (typeof value === "object") return extractTextFromNode(value).replace(/\s+/g, " ").trim();
86
+ return String(value);
87
+ }
88
+ function RichTextCell({ value }) {
89
+ const text = getRichTextPreview(value);
90
+ if (!text) return /* @__PURE__ */ jsx("span", {
91
+ className: "text-muted-foreground",
92
+ children: "-"
93
+ });
94
+ return /* @__PURE__ */ jsx("span", {
95
+ className: "truncate max-w-[300px] block",
96
+ title: text,
97
+ children: text
98
+ });
99
+ }
100
+ /**
101
+ * Number cell - right-aligned number display
102
+ */
103
+ function NumberCell({ value }) {
104
+ if (value === null || value === void 0) return /* @__PURE__ */ jsx("span", {
105
+ className: "text-muted-foreground",
106
+ children: "-"
107
+ });
108
+ const num = Number(value);
109
+ return /* @__PURE__ */ jsx("span", {
110
+ className: "tabular-nums",
111
+ children: Number.isNaN(num) ? String(value) : num.toLocaleString()
112
+ });
113
+ }
114
+ /**
115
+ * Boolean cell - badge display
116
+ */
117
+ function BooleanCell({ value }) {
118
+ return /* @__PURE__ */ jsx(Badge, {
119
+ variant: value ? "default" : "secondary",
120
+ children: value ? "Yes" : "No"
121
+ });
122
+ }
123
+ /**
124
+ * Date cell - formatted date display
125
+ */
126
+ function DateCell({ value }) {
127
+ if (value === null || value === void 0) return /* @__PURE__ */ jsx("span", {
128
+ className: "text-muted-foreground",
129
+ children: "-"
130
+ });
131
+ const date = value instanceof Date ? value : new Date(String(value));
132
+ if (Number.isNaN(date.getTime())) return /* @__PURE__ */ jsx("span", {
133
+ className: "text-muted-foreground",
134
+ children: String(value)
135
+ });
136
+ return /* @__PURE__ */ jsx("span", {
137
+ className: "tabular-nums",
138
+ children: date.toLocaleDateString()
139
+ });
140
+ }
141
+ /**
142
+ * DateTime cell - formatted date and time display
143
+ */
144
+ function DateTimeCell({ value }) {
145
+ if (value === null || value === void 0) return /* @__PURE__ */ jsx("span", {
146
+ className: "text-muted-foreground",
147
+ children: "-"
148
+ });
149
+ const date = value instanceof Date ? value : new Date(String(value));
150
+ if (Number.isNaN(date.getTime())) return /* @__PURE__ */ jsx("span", {
151
+ className: "text-muted-foreground",
152
+ children: String(value)
153
+ });
154
+ return /* @__PURE__ */ jsxs("span", {
155
+ className: "tabular-nums",
156
+ children: [
157
+ date.toLocaleDateString(),
158
+ " ",
159
+ date.toLocaleTimeString()
160
+ ]
161
+ });
162
+ }
163
+ /**
164
+ * Time cell - formatted time display (for time-only values stored as string)
165
+ */
166
+ function TimeCell({ value }) {
167
+ if (value === null || value === void 0 || value === "") return /* @__PURE__ */ jsx("span", {
168
+ className: "text-muted-foreground",
169
+ children: "-"
170
+ });
171
+ return /* @__PURE__ */ jsx("span", {
172
+ className: "tabular-nums",
173
+ children: String(value)
174
+ });
175
+ }
176
+ /**
177
+ * Email cell - displays email with mailto link styling
178
+ */
179
+ function EmailCell({ value }) {
180
+ if (value === null || value === void 0 || value === "") return /* @__PURE__ */ jsx("span", {
181
+ className: "text-muted-foreground",
182
+ children: "-"
183
+ });
184
+ return /* @__PURE__ */ jsx("span", {
185
+ className: "text-primary",
186
+ children: String(value)
187
+ });
188
+ }
189
+ /**
190
+ * Select/Status cell - badge display
191
+ */
192
+ function SelectCell({ value }) {
193
+ if (value === null || value === void 0 || value === "") return /* @__PURE__ */ jsx("span", {
194
+ className: "text-muted-foreground",
195
+ children: "-"
196
+ });
197
+ return /* @__PURE__ */ jsx(Badge, {
198
+ variant: "outline",
199
+ children: String(value)
200
+ });
201
+ }
202
+
203
+ //#endregion
204
+ //#region src/client/components/ui/accordion.tsx
205
+ function Accordion$1({ className, ...props }) {
206
+ return /* @__PURE__ */ jsx(Accordion.Root, {
207
+ "data-slot": "accordion",
208
+ className: cn("overflow-hidden border border-border/60 bg-card/30 backdrop-blur-md flex w-full flex-col", className),
209
+ ...props
210
+ });
211
+ }
212
+ function AccordionItem({ className, ...props }) {
213
+ return /* @__PURE__ */ jsx(Accordion.Item, {
214
+ "data-slot": "accordion-item",
215
+ className: cn("data-open:bg-muted/30 data-open:backdrop-blur-sm not-last:border-b border-border/40 transition-colors", className),
216
+ ...props
217
+ });
218
+ }
219
+ function AccordionTrigger({ className, children, ...props }) {
220
+ return /* @__PURE__ */ jsx(Accordion.Header, {
221
+ className: "flex",
222
+ children: /* @__PURE__ */ jsxs(Accordion.Trigger, {
223
+ "data-slot": "accordion-trigger",
224
+ className: cn("**:data-[slot=accordion-trigger-icon]:text-muted-foreground gap-4 px-4 py-3 text-left text-sm font-medium hover:bg-muted/20 **:data-[slot=accordion-trigger-icon]:ml-auto **:data-[slot=accordion-trigger-icon]:size-4 group/accordion-trigger relative flex flex-1 items-center justify-between transition-all outline-none disabled:pointer-events-none disabled:opacity-50", className),
225
+ ...props,
226
+ children: [children, /* @__PURE__ */ jsx(CaretDownIcon, {
227
+ "data-slot": "accordion-trigger-icon",
228
+ className: "pointer-events-none shrink-0 transition-transform duration-200 group-aria-expanded/accordion-trigger:rotate-180"
229
+ })]
230
+ })
231
+ });
232
+ }
233
+ function AccordionContent({ className, children, ...props }) {
234
+ return /* @__PURE__ */ jsx(Accordion.Panel, {
235
+ "data-slot": "accordion-content",
236
+ className: "data-open:animate-accordion-down data-closed:animate-accordion-up overflow-hidden",
237
+ ...props,
238
+ children: /* @__PURE__ */ jsx("div", {
239
+ className: cn("px-4 pt-0 pb-4 text-sm [&_a]:hover:text-foreground h-(--accordion-panel-height) data-ending-style:h-0 data-starting-style:h-0 [&_a]:underline [&_a]:underline-offset-3 [&_p:not(:last-child)]:mb-4", className),
240
+ children
241
+ })
242
+ });
243
+ }
244
+
245
+ //#endregion
246
+ //#region src/client/components/ui/tabs.tsx
247
+ function Tabs$1({ className, orientation = "horizontal", ...props }) {
248
+ return /* @__PURE__ */ jsx(Tabs.Root, {
249
+ "data-slot": "tabs",
250
+ "data-orientation": orientation,
251
+ className: cn("gap-2 group/tabs flex data-[orientation=horizontal]:flex-col", className),
252
+ ...props
253
+ });
254
+ }
255
+ const tabsListVariants = cva("p-[3px] group-data-horizontal/tabs:h-8 group/tabs-list text-muted-foreground inline-flex w-fit items-center justify-center group-data-[orientation=vertical]/tabs:h-fit group-data-[orientation=vertical]/tabs:flex-col", {
256
+ variants: { variant: {
257
+ default: "bg-muted",
258
+ line: "gap-1 bg-transparent"
259
+ } },
260
+ defaultVariants: { variant: "default" }
261
+ });
262
+ function TabsList({ className, variant = "default", ...props }) {
263
+ return /* @__PURE__ */ jsx(Tabs.List, {
264
+ "data-slot": "tabs-list",
265
+ "data-variant": variant,
266
+ className: cn(tabsListVariants({ variant }), className),
267
+ ...props
268
+ });
269
+ }
270
+ function TabsTrigger({ className, ...props }) {
271
+ return /* @__PURE__ */ jsx(Tabs.Tab, {
272
+ "data-slot": "tabs-trigger",
273
+ className: cn("gap-1.5 border border-transparent px-1.5 py-0.5 text-xs font-medium group-data-vertical/tabs:py-[calc(--spacing(1.25))] [&_svg:not([class*='size-'])]:size-3.5 focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:outline-ring text-foreground/60 hover:text-foreground dark:text-muted-foreground dark:hover:text-foreground relative inline-flex h-[calc(100%-1px)] flex-1 items-center justify-center whitespace-nowrap transition-all group-data-[orientation=vertical]/tabs:w-full group-data-[orientation=vertical]/tabs:justify-start focus-visible:ring-[3px] focus-visible:outline-1 disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0", "group-data-[variant=line]/tabs-list:bg-transparent group-data-[variant=line]/tabs-list:data-active:bg-transparent dark:group-data-[variant=line]/tabs-list:data-active:border-transparent dark:group-data-[variant=line]/tabs-list:data-active:bg-transparent", "data-active:bg-background dark:data-active:text-foreground dark:data-active:border-input dark:data-active:bg-input/30 data-active:text-foreground", "after:bg-foreground after:absolute after:opacity-0 after:transition-opacity group-data-[orientation=horizontal]/tabs:after:inset-x-0 group-data-[orientation=horizontal]/tabs:after:bottom-[-5px] group-data-[orientation=horizontal]/tabs:after:h-0.5 group-data-[orientation=vertical]/tabs:after:inset-y-0 group-data-[orientation=vertical]/tabs:after:-right-1 group-data-[orientation=vertical]/tabs:after:w-0.5 group-data-[variant=line]/tabs-list:data-active:after:opacity-100", className),
274
+ ...props
275
+ });
276
+ }
277
+ function TabsContent({ className, ...props }) {
278
+ return /* @__PURE__ */ jsx(Tabs.Panel, {
279
+ "data-slot": "tabs-content",
280
+ className: cn("text-xs/relaxed flex-1 outline-none", className),
281
+ ...props
282
+ });
283
+ }
284
+
285
+ //#endregion
286
+ //#region src/client/hooks/use-collection.ts
287
+ /**
288
+ * Hook to fetch collection list with filters, sorting, pagination
289
+ *
290
+ * Uses RegisteredCMS from module augmentation for automatic type inference.
291
+ *
292
+ * @example
293
+ * ```tsx
294
+ * // Types inferred from module augmentation!
295
+ * const { data } = useCollectionList("barbers");
296
+ * ```
297
+ */
298
+ function useCollectionList(collection, options, queryOptions) {
299
+ const client = useAdminStore(selectClient);
300
+ const { locale: contentLocale } = useScopedLocale();
301
+ return useQuery({
302
+ ...createQuestpieQueryOptions(client, {
303
+ keyPrefix: ["questpie", "collections"],
304
+ locale: contentLocale
305
+ }).collections[collection].find({
306
+ ...options,
307
+ locale: contentLocale
308
+ }),
309
+ ...queryOptions
310
+ });
311
+ }
312
+ /**
313
+ * Hook to count collection items with optional filters
314
+ *
315
+ * More efficient than useCollectionList when you only need the count.
316
+ * Uses dedicated count endpoint that doesn't fetch actual documents.
317
+ *
318
+ * @example
319
+ * ```tsx
320
+ * // Count all items
321
+ * const { data: count } = useCollectionCount("barbers");
322
+ *
323
+ * // Count with filter
324
+ * const { data: count } = useCollectionCount("appointments", {
325
+ * where: { status: "pending" }
326
+ * });
327
+ * ```
328
+ */
329
+ function useCollectionCount(collection, options, queryOptions) {
330
+ const client = useAdminStore(selectClient);
331
+ const { locale: contentLocale } = useScopedLocale();
332
+ return useQuery({
333
+ ...createQuestpieQueryOptions(client, {
334
+ keyPrefix: ["questpie", "collections"],
335
+ locale: contentLocale
336
+ }).collections[collection].count({
337
+ ...options,
338
+ locale: contentLocale
339
+ }),
340
+ ...queryOptions
341
+ });
342
+ }
343
+ /**
344
+ * Hook to fetch single collection item
345
+ *
346
+ * Uses RegisteredCMS from module augmentation for automatic type inference.
347
+ *
348
+ * @example
349
+ * ```tsx
350
+ * // Types inferred from module augmentation!
351
+ * const { data } = useCollectionItem("barbers", "123");
352
+ * ```
353
+ */
354
+ function useCollectionItem(collection, id, options, queryOptions) {
355
+ const client = useAdminStore(selectClient);
356
+ const { locale: contentLocale } = useScopedLocale();
357
+ return useQuery({
358
+ ...createQuestpieQueryOptions(client, {
359
+ keyPrefix: ["questpie", "collections"],
360
+ locale: contentLocale
361
+ }).collections[collection].findOne({
362
+ where: { id },
363
+ locale: contentLocale,
364
+ ...options
365
+ }),
366
+ ...queryOptions
367
+ });
368
+ }
369
+ /**
370
+ * Hook to create collection item
371
+ *
372
+ * Uses RegisteredCMS from module augmentation for automatic type inference.
373
+ *
374
+ * @example
375
+ * ```tsx
376
+ * // Types inferred from module augmentation!
377
+ * const { mutate } = useCollectionCreate("barbers");
378
+ * mutate({ name: "John", ... });
379
+ * ```
380
+ */
381
+ function useCollectionCreate(collection, mutationOptions) {
382
+ const client = useAdminStore(selectClient);
383
+ const { locale: contentLocale } = useScopedLocale();
384
+ const queryClient = useQueryClient();
385
+ const queryOpts = createQuestpieQueryOptions(client, {
386
+ keyPrefix: ["questpie", "collections"],
387
+ locale: contentLocale
388
+ });
389
+ const baseOptions = queryOpts.collections[collection].create();
390
+ const listQueryKey = queryOpts.key([
391
+ "collections",
392
+ collection,
393
+ "find",
394
+ contentLocale
395
+ ]);
396
+ const countQueryKey = queryOpts.key([
397
+ "collections",
398
+ collection,
399
+ "count",
400
+ contentLocale
401
+ ]);
402
+ return useMutation({
403
+ ...baseOptions,
404
+ onSuccess: (data, variables, context) => {
405
+ queryClient.invalidateQueries({ queryKey: listQueryKey });
406
+ queryClient.invalidateQueries({ queryKey: countQueryKey });
407
+ (mutationOptions?.onSuccess)?.(data, variables, context);
408
+ },
409
+ onSettled: (data, error, variables, context) => {
410
+ queryClient.invalidateQueries({ queryKey: listQueryKey });
411
+ queryClient.invalidateQueries({ queryKey: countQueryKey });
412
+ (mutationOptions?.onSettled)?.(data, error, variables, context);
413
+ },
414
+ ...mutationOptions
415
+ });
416
+ }
417
+ /**
418
+ * Hook to update collection item
419
+ *
420
+ * Uses RegisteredCMS from module augmentation for automatic type inference.
421
+ *
422
+ * @example
423
+ * ```tsx
424
+ * // Types inferred from module augmentation!
425
+ * const { mutate } = useCollectionUpdate("barbers");
426
+ * mutate({ id: "123", data: { name: "John" } });
427
+ * ```
428
+ */
429
+ function useCollectionUpdate(collection, mutationOptions) {
430
+ const client = useAdminStore(selectClient);
431
+ const { locale: contentLocale } = useScopedLocale();
432
+ const queryClient = useQueryClient();
433
+ const queryOpts = createQuestpieQueryOptions(client, {
434
+ keyPrefix: ["questpie", "collections"],
435
+ locale: contentLocale
436
+ });
437
+ const baseOptions = queryOpts.collections[collection].update();
438
+ const listQueryKey = queryOpts.key([
439
+ "collections",
440
+ collection,
441
+ "find",
442
+ contentLocale
443
+ ]);
444
+ const countQueryKey = queryOpts.key([
445
+ "collections",
446
+ collection,
447
+ "count",
448
+ contentLocale
449
+ ]);
450
+ const itemQueryKey = queryOpts.key([
451
+ "collections",
452
+ collection,
453
+ "findOne",
454
+ contentLocale
455
+ ]);
456
+ return useMutation({
457
+ ...baseOptions,
458
+ onSuccess: (data, variables, context) => {
459
+ queryClient.invalidateQueries({ queryKey: listQueryKey });
460
+ queryClient.invalidateQueries({ queryKey: countQueryKey });
461
+ queryClient.invalidateQueries({ queryKey: itemQueryKey });
462
+ (mutationOptions?.onSuccess)?.(data, variables, context);
463
+ },
464
+ onSettled: (data, error, variables, context) => {
465
+ queryClient.invalidateQueries({ queryKey: listQueryKey });
466
+ queryClient.invalidateQueries({ queryKey: countQueryKey });
467
+ queryClient.invalidateQueries({ queryKey: itemQueryKey });
468
+ (mutationOptions?.onSettled)?.(data, error, variables, context);
469
+ },
470
+ ...mutationOptions
471
+ });
472
+ }
473
+ /**
474
+ * Hook to delete collection item
475
+ *
476
+ * Uses RegisteredCMS from module augmentation for automatic type inference.
477
+ *
478
+ * @example
479
+ * ```tsx
480
+ * // Types inferred from module augmentation!
481
+ * const { mutate } = useCollectionDelete("barbers");
482
+ * mutate("123");
483
+ * ```
484
+ */
485
+ function useCollectionDelete(collection, mutationOptions) {
486
+ const client = useAdminStore(selectClient);
487
+ const { locale: contentLocale } = useScopedLocale();
488
+ const queryClient = useQueryClient();
489
+ const queryOpts = createQuestpieQueryOptions(client, {
490
+ keyPrefix: ["questpie", "collections"],
491
+ locale: contentLocale
492
+ });
493
+ const baseOptions = queryOpts.collections[collection].delete();
494
+ const listQueryKey = queryOpts.key([
495
+ "collections",
496
+ collection,
497
+ "find",
498
+ contentLocale
499
+ ]);
500
+ const countQueryKey = queryOpts.key([
501
+ "collections",
502
+ collection,
503
+ "count",
504
+ contentLocale
505
+ ]);
506
+ const itemQueryKey = queryOpts.key([
507
+ "collections",
508
+ collection,
509
+ "findOne",
510
+ contentLocale
511
+ ]);
512
+ return useMutation({
513
+ ...baseOptions,
514
+ onSuccess: (data, variables, context) => {
515
+ queryClient.invalidateQueries({ queryKey: listQueryKey });
516
+ queryClient.invalidateQueries({ queryKey: countQueryKey });
517
+ queryClient.invalidateQueries({ queryKey: itemQueryKey });
518
+ (mutationOptions?.onSuccess)?.(data, variables, context);
519
+ },
520
+ onSettled: (data, error, variables, context) => {
521
+ queryClient.invalidateQueries({ queryKey: listQueryKey });
522
+ queryClient.invalidateQueries({ queryKey: countQueryKey });
523
+ queryClient.invalidateQueries({ queryKey: itemQueryKey });
524
+ (mutationOptions?.onSettled)?.(data, error, variables, context);
525
+ },
526
+ ...mutationOptions
527
+ });
528
+ }
529
+
530
+ //#endregion
531
+ //#region src/client/components/ui/skeleton.tsx
532
+ function Skeleton({ className, ...props }) {
533
+ return /* @__PURE__ */ jsx("div", {
534
+ "data-slot": "skeleton",
535
+ className: cn("bg-muted animate-pulse", className),
536
+ ...props
537
+ });
538
+ }
539
+
540
+ //#endregion
541
+ //#region src/client/components/error-boundary.tsx
542
+ /**
543
+ * ErrorBoundary - Catches JavaScript errors in child component tree
544
+ *
545
+ * Prevents entire UI from crashing when a component throws an error.
546
+ * Use around widgets, sections, or any component that might fail.
547
+ *
548
+ * @example
549
+ * ```tsx
550
+ * <ErrorBoundary fallback={<WidgetError />}>
551
+ * <ChartWidget config={config} />
552
+ * </ErrorBoundary>
553
+ *
554
+ * // With error callback
555
+ * <ErrorBoundary
556
+ * fallback={(error) => <p>Error: {error.message}</p>}
557
+ * onError={(error) => logToService(error)}
558
+ * >
559
+ * <DashboardWidget config={config} />
560
+ * </ErrorBoundary>
561
+ * ```
562
+ */
563
+ var ErrorBoundary = class extends React$1.Component {
564
+ constructor(props) {
565
+ super(props);
566
+ this.state = {
567
+ hasError: false,
568
+ error: null
569
+ };
570
+ }
571
+ static getDerivedStateFromError(error) {
572
+ return {
573
+ hasError: true,
574
+ error
575
+ };
576
+ }
577
+ componentDidCatch(error, errorInfo) {
578
+ this.props.onError?.(error, errorInfo);
579
+ }
580
+ render() {
581
+ if (this.state.hasError && this.state.error) {
582
+ const { fallback } = this.props;
583
+ if (typeof fallback === "function") return fallback(this.state.error);
584
+ if (fallback) return fallback;
585
+ return /* @__PURE__ */ jsxs("div", {
586
+ className: "rounded-lg border border-destructive/20 bg-destructive/5 p-4",
587
+ children: [/* @__PURE__ */ jsx("p", {
588
+ className: "text-sm font-medium text-destructive",
589
+ children: "Something went wrong"
590
+ }), /* @__PURE__ */ jsx("p", {
591
+ className: "mt-1 text-xs text-muted-foreground",
592
+ children: this.state.error.message
593
+ })]
594
+ });
595
+ }
596
+ return this.props.children;
597
+ }
598
+ };
599
+ /**
600
+ * WidgetErrorBoundary - Specialized error boundary for dashboard widgets
601
+ */
602
+ function WidgetErrorBoundary({ children, widgetType }) {
603
+ return /* @__PURE__ */ jsx(ErrorBoundary, {
604
+ fallback: (error) => /* @__PURE__ */ jsxs("div", {
605
+ className: "rounded-lg border border-destructive/20 bg-destructive/5 p-4",
606
+ children: [
607
+ /* @__PURE__ */ jsx("p", {
608
+ className: "text-sm font-medium text-destructive",
609
+ children: "Widget Error"
610
+ }),
611
+ widgetType && /* @__PURE__ */ jsxs("p", {
612
+ className: "text-xs text-muted-foreground",
613
+ children: ["Type: ", widgetType]
614
+ }),
615
+ /* @__PURE__ */ jsx("p", {
616
+ className: "mt-1 text-xs text-muted-foreground",
617
+ children: error.message
618
+ })
619
+ ]
620
+ }),
621
+ children
622
+ });
623
+ }
624
+
625
+ //#endregion
626
+ //#region src/client/views/dashboard/widget-card.tsx
627
+ /**
628
+ * WidgetCard Component
629
+ *
630
+ * Standardized card wrapper for dashboard widgets with multiple variants.
631
+ * Provides consistent styling, header actions, and loading/error states.
632
+ */
633
+ const variantStyles$2 = {
634
+ default: "",
635
+ compact: "py-3 gap-3",
636
+ featured: "border-primary/30 bg-gradient-to-br from-primary/5 to-transparent shadow-sm"
637
+ };
638
+ const variantContentStyles = {
639
+ default: "",
640
+ compact: "pt-0",
641
+ featured: ""
642
+ };
643
+ function WidgetCardLoading({ variant = "default" }) {
644
+ return /* @__PURE__ */ jsxs(Card, {
645
+ className: cn("h-full flex flex-col", variantStyles$2[variant]),
646
+ children: [/* @__PURE__ */ jsx(CardHeader, { children: /* @__PURE__ */ jsx(Skeleton, { className: "h-4 w-24" }) }), /* @__PURE__ */ jsx(CardContent, {
647
+ className: cn("flex-1", variantContentStyles[variant]),
648
+ children: /* @__PURE__ */ jsx(Skeleton, { className: "h-20 w-full" })
649
+ })]
650
+ });
651
+ }
652
+ function WidgetCardError({ error, variant = "default", onRetry }) {
653
+ return /* @__PURE__ */ jsxs(Card, {
654
+ className: cn("h-full flex flex-col border-destructive/20 bg-destructive/5", variantStyles$2[variant]),
655
+ children: [/* @__PURE__ */ jsxs(CardHeader, { children: [/* @__PURE__ */ jsx(CardTitle, {
656
+ className: "text-sm font-medium text-destructive",
657
+ children: "Error"
658
+ }), onRetry && /* @__PURE__ */ jsx(CardAction, { children: /* @__PURE__ */ jsx(Button, {
659
+ variant: "ghost",
660
+ size: "icon-xs",
661
+ onClick: onRetry,
662
+ children: /* @__PURE__ */ jsx(ArrowClockwise, { className: "h-3.5 w-3.5" })
663
+ }) })] }), /* @__PURE__ */ jsx(CardContent, {
664
+ className: cn("flex-1", variantContentStyles[variant]),
665
+ children: /* @__PURE__ */ jsx("p", {
666
+ className: "text-xs text-muted-foreground",
667
+ children: error.message
668
+ })
669
+ })]
670
+ });
671
+ }
672
+ /**
673
+ * WidgetCard - Standardized card wrapper for dashboard widgets
674
+ *
675
+ * @example
676
+ * ```tsx
677
+ * <WidgetCard
678
+ * title="Revenue"
679
+ * description="Monthly sales total"
680
+ * variant="featured"
681
+ * onRefresh={() => refetch()}
682
+ * >
683
+ * <div className="text-2xl font-bold">$12,345</div>
684
+ * </WidgetCard>
685
+ * ```
686
+ */
687
+ function WidgetCard({ title, description, icon: Icon, variant = "default", isLoading, isRefreshing, error, onRefresh, onExpand, actions, className, loadingSkeleton, children }) {
688
+ if (isLoading) {
689
+ if (loadingSkeleton) return /* @__PURE__ */ jsxs(Card, {
690
+ className: cn("h-full flex flex-col", variantStyles$2[variant], className),
691
+ children: [/* @__PURE__ */ jsx(CardHeader, { children: /* @__PURE__ */ jsx(Skeleton, { className: "h-4 w-24" }) }), /* @__PURE__ */ jsx(CardContent, {
692
+ className: cn("flex-1", variantContentStyles[variant]),
693
+ children: loadingSkeleton
694
+ })]
695
+ });
696
+ return /* @__PURE__ */ jsx(WidgetCardLoading, { variant });
697
+ }
698
+ if (error) return /* @__PURE__ */ jsx(WidgetCardError, {
699
+ error,
700
+ variant,
701
+ onRetry: onRefresh
702
+ });
703
+ const hasHeader = title || description || Icon || onRefresh || onExpand || actions?.length;
704
+ return /* @__PURE__ */ jsxs(Card, {
705
+ className: cn("h-full flex flex-col", variantStyles$2[variant], className),
706
+ children: [hasHeader && /* @__PURE__ */ jsxs(CardHeader, { children: [/* @__PURE__ */ jsxs("div", {
707
+ className: "flex items-center gap-2",
708
+ children: [Icon && /* @__PURE__ */ jsx(Icon, { className: "h-4 w-4 text-muted-foreground" }), /* @__PURE__ */ jsxs("div", {
709
+ className: "flex-1 min-w-0",
710
+ children: [title && /* @__PURE__ */ jsx(CardTitle, {
711
+ className: "text-sm font-medium truncate",
712
+ children: title
713
+ }), description && /* @__PURE__ */ jsx(CardDescription, {
714
+ className: "truncate",
715
+ children: description
716
+ })]
717
+ })]
718
+ }), (onRefresh || onExpand || actions?.length) && /* @__PURE__ */ jsx(CardAction, { children: /* @__PURE__ */ jsxs("div", {
719
+ className: "flex items-center gap-1",
720
+ children: [
721
+ actions?.map((action) => /* @__PURE__ */ jsx(Button, {
722
+ variant: "ghost",
723
+ size: "icon-xs",
724
+ onClick: action.onClick,
725
+ title: action.label,
726
+ children: action.icon && typeof action.icon !== "string" && /* @__PURE__ */ jsx(action.icon, { className: "h-3.5 w-3.5" })
727
+ }, action.id)),
728
+ onRefresh && /* @__PURE__ */ jsx(Button, {
729
+ variant: "ghost",
730
+ size: "icon-xs",
731
+ onClick: onRefresh,
732
+ title: "Refresh",
733
+ disabled: isRefreshing,
734
+ children: /* @__PURE__ */ jsx(ArrowClockwise, { className: cn("h-3.5 w-3.5", isRefreshing && "animate-spin") })
735
+ }),
736
+ onExpand && /* @__PURE__ */ jsx(Button, {
737
+ variant: "ghost",
738
+ size: "icon-xs",
739
+ onClick: onExpand,
740
+ title: "Expand",
741
+ children: /* @__PURE__ */ jsx(ArrowsOutSimple, { className: "h-3.5 w-3.5" })
742
+ })
743
+ ]
744
+ }) })] }), /* @__PURE__ */ jsx(CardContent, {
745
+ className: cn("flex-1", variantContentStyles[variant], !hasHeader && "pt-0"),
746
+ children
747
+ })]
748
+ });
749
+ }
750
+
751
+ //#endregion
752
+ //#region src/client/components/widgets/widget-skeletons.tsx
753
+ /**
754
+ * Widget Skeletons
755
+ *
756
+ * Loading skeleton components for different widget types.
757
+ * Provides visual feedback while data is being fetched.
758
+ */
759
+ function StatsWidgetSkeleton() {
760
+ return /* @__PURE__ */ jsxs("div", {
761
+ className: "space-y-3",
762
+ children: [/* @__PURE__ */ jsx(Skeleton, { className: "h-8 w-20" }), /* @__PURE__ */ jsx(Skeleton, { className: "h-4 w-32" })]
763
+ });
764
+ }
765
+ function ChartWidgetSkeleton() {
766
+ return /* @__PURE__ */ jsxs("div", {
767
+ className: "h-48 w-full flex items-end gap-2 pt-4",
768
+ children: [
769
+ /* @__PURE__ */ jsx(Skeleton, { className: "flex-1 rounded-t h-[40%]" }),
770
+ /* @__PURE__ */ jsx(Skeleton, { className: "flex-1 rounded-t h-[65%]" }),
771
+ /* @__PURE__ */ jsx(Skeleton, { className: "flex-1 rounded-t h-[45%]" }),
772
+ /* @__PURE__ */ jsx(Skeleton, { className: "flex-1 rounded-t h-[80%]" }),
773
+ /* @__PURE__ */ jsx(Skeleton, { className: "flex-1 rounded-t h-[55%]" }),
774
+ /* @__PURE__ */ jsx(Skeleton, { className: "flex-1 rounded-t h-[70%]" }),
775
+ /* @__PURE__ */ jsx(Skeleton, { className: "flex-1 rounded-t h-[50%]" }),
776
+ /* @__PURE__ */ jsx(Skeleton, { className: "flex-1 rounded-t h-[75%]" }),
777
+ /* @__PURE__ */ jsx(Skeleton, { className: "flex-1 rounded-t h-[60%]" }),
778
+ /* @__PURE__ */ jsx(Skeleton, { className: "flex-1 rounded-t h-[85%]" })
779
+ ]
780
+ });
781
+ }
782
+ function RecentItemsWidgetSkeleton({ count = 5 }) {
783
+ return /* @__PURE__ */ jsxs("div", {
784
+ className: "space-y-1",
785
+ children: [
786
+ /* @__PURE__ */ jsx(SkeletonRow, {}),
787
+ count > 1 && /* @__PURE__ */ jsx(SkeletonRow, {}),
788
+ count > 2 && /* @__PURE__ */ jsx(SkeletonRow, {}),
789
+ count > 3 && /* @__PURE__ */ jsx(SkeletonRow, {}),
790
+ count > 4 && /* @__PURE__ */ jsx(SkeletonRow, {})
791
+ ]
792
+ });
793
+ }
794
+ function SkeletonRow() {
795
+ return /* @__PURE__ */ jsx("div", {
796
+ className: "flex items-center gap-3 p-2",
797
+ children: /* @__PURE__ */ jsxs("div", {
798
+ className: "flex-1 space-y-2",
799
+ children: [/* @__PURE__ */ jsx(Skeleton, { className: "h-4 w-3/4" }), /* @__PURE__ */ jsx(Skeleton, { className: "h-3 w-1/2" })]
800
+ })
801
+ });
802
+ }
803
+ function TableWidgetSkeleton({ rows = 5, columns = 3 }) {
804
+ return /* @__PURE__ */ jsxs("div", {
805
+ className: "-mx-5",
806
+ children: [
807
+ /* @__PURE__ */ jsxs("div", {
808
+ className: "flex gap-4 px-5 py-2 border-b border-border/50",
809
+ children: [
810
+ /* @__PURE__ */ jsx(Skeleton, { className: "h-4 w-24" }),
811
+ columns > 1 && /* @__PURE__ */ jsx(Skeleton, { className: "h-4 flex-1" }),
812
+ columns > 2 && /* @__PURE__ */ jsx(Skeleton, { className: "h-4 flex-1" })
813
+ ]
814
+ }),
815
+ /* @__PURE__ */ jsx(TableSkeletonRow, { columns }),
816
+ rows > 1 && /* @__PURE__ */ jsx(TableSkeletonRow, { columns }),
817
+ rows > 2 && /* @__PURE__ */ jsx(TableSkeletonRow, { columns }),
818
+ rows > 3 && /* @__PURE__ */ jsx(TableSkeletonRow, { columns }),
819
+ rows > 4 && /* @__PURE__ */ jsx(TableSkeletonRow, {
820
+ columns,
821
+ last: true
822
+ })
823
+ ]
824
+ });
825
+ }
826
+ function TableSkeletonRow({ columns = 3, last = false }) {
827
+ return /* @__PURE__ */ jsxs("div", {
828
+ className: cn("flex gap-4 px-5 py-3 border-b border-border/30", last && "border-0"),
829
+ children: [
830
+ /* @__PURE__ */ jsx(Skeleton, { className: "h-4 w-20" }),
831
+ columns > 1 && /* @__PURE__ */ jsx(Skeleton, { className: "h-4 flex-1" }),
832
+ columns > 2 && /* @__PURE__ */ jsx(Skeleton, { className: "h-4 flex-1" })
833
+ ]
834
+ });
835
+ }
836
+ function TimelineWidgetSkeleton({ count = 5 }) {
837
+ return /* @__PURE__ */ jsxs("div", {
838
+ className: "space-y-4",
839
+ children: [
840
+ /* @__PURE__ */ jsx(TimelineSkeletonItem, {}),
841
+ count > 1 && /* @__PURE__ */ jsx(TimelineSkeletonItem, {}),
842
+ count > 2 && /* @__PURE__ */ jsx(TimelineSkeletonItem, {}),
843
+ count > 3 && /* @__PURE__ */ jsx(TimelineSkeletonItem, {}),
844
+ count > 4 && /* @__PURE__ */ jsx(TimelineSkeletonItem, { last: true })
845
+ ]
846
+ });
847
+ }
848
+ function TimelineSkeletonItem({ last = false }) {
849
+ return /* @__PURE__ */ jsxs("div", {
850
+ className: "flex gap-3",
851
+ children: [/* @__PURE__ */ jsxs("div", {
852
+ className: "flex flex-col items-center",
853
+ children: [/* @__PURE__ */ jsx(Skeleton, { className: "h-3 w-3 rounded-full" }), !last && /* @__PURE__ */ jsx(Skeleton, { className: "w-0.5 flex-1 mt-1" })]
854
+ }), /* @__PURE__ */ jsxs("div", {
855
+ className: "flex-1 space-y-1 pb-4",
856
+ children: [
857
+ /* @__PURE__ */ jsx(Skeleton, { className: "h-4 w-3/4" }),
858
+ /* @__PURE__ */ jsx(Skeleton, { className: "h-3 w-1/2" }),
859
+ /* @__PURE__ */ jsx(Skeleton, { className: "h-3 w-20" })
860
+ ]
861
+ })]
862
+ });
863
+ }
864
+ function ProgressWidgetSkeleton() {
865
+ return /* @__PURE__ */ jsxs("div", {
866
+ className: "space-y-3",
867
+ children: [
868
+ /* @__PURE__ */ jsxs("div", {
869
+ className: "flex justify-between items-center",
870
+ children: [/* @__PURE__ */ jsx(Skeleton, { className: "h-4 w-32" }), /* @__PURE__ */ jsx(Skeleton, { className: "h-4 w-12" })]
871
+ }),
872
+ /* @__PURE__ */ jsx(Skeleton, { className: "h-2 w-full rounded-full" }),
873
+ /* @__PURE__ */ jsx(Skeleton, { className: "h-3 w-24" })
874
+ ]
875
+ });
876
+ }
877
+ function ValueWidgetSkeleton({ featured = false }) {
878
+ return /* @__PURE__ */ jsxs("div", {
879
+ className: "space-y-3",
880
+ children: [/* @__PURE__ */ jsxs("div", {
881
+ className: "flex items-start gap-3",
882
+ children: [/* @__PURE__ */ jsx(Skeleton, { className: "h-10 w-10 rounded-md shrink-0" }), /* @__PURE__ */ jsxs("div", {
883
+ className: "flex-1 space-y-2",
884
+ children: [/* @__PURE__ */ jsx(Skeleton, { className: "h-4 w-24" }), /* @__PURE__ */ jsx(Skeleton, { className: cn("h-8", featured ? "w-40" : "w-24") })]
885
+ })]
886
+ }), /* @__PURE__ */ jsx(Skeleton, { className: "h-3 w-32" })]
887
+ });
888
+ }
889
+
890
+ //#endregion
891
+ //#region src/client/components/widgets/chart-widget.tsx
892
+ /**
893
+ * Chart Widget
894
+ *
895
+ * Displays data visualization over time.
896
+ * Uses WidgetCard for consistent styling.
897
+ */
898
+ const CHART_COLORS = [
899
+ "var(--color-chart-1)",
900
+ "var(--color-chart-2)",
901
+ "var(--color-chart-3)",
902
+ "var(--color-chart-4)",
903
+ "var(--color-chart-5)"
904
+ ];
905
+ /**
906
+ * Custom tooltip component matching shadcn style
907
+ */
908
+ function ChartTooltip({ active, payload, label }) {
909
+ if (!active || !payload?.length) return null;
910
+ return /* @__PURE__ */ jsxs("div", {
911
+ className: "rounded-md border border-border bg-background px-3 py-2 text-xs shadow-md",
912
+ children: [/* @__PURE__ */ jsx("p", {
913
+ className: "font-medium text-foreground",
914
+ children: label
915
+ }), payload.map((entry) => /* @__PURE__ */ jsxs("p", {
916
+ className: "text-muted-foreground",
917
+ children: [
918
+ entry.name,
919
+ ":",
920
+ " ",
921
+ /* @__PURE__ */ jsx("span", {
922
+ className: "font-medium text-foreground",
923
+ children: entry.value
924
+ })
925
+ ]
926
+ }, String(entry.name)))]
927
+ });
928
+ }
929
+ /**
930
+ * Chart widget component
931
+ *
932
+ * Displays:
933
+ * - Time series data visualization
934
+ * - Multiple chart types (line, bar, area, pie)
935
+ * - Configurable time ranges
936
+ */
937
+ function ChartWidget({ config }) {
938
+ const resolveText = useResolveText();
939
+ const { collection, field, chartType = "area", timeRange = "30d", label, color = "var(--color-chart-1)", showGrid = true } = config;
940
+ const { data, isLoading, error, refetch } = useCollectionList(collection, { limit: 1e3 });
941
+ const items = Array.isArray(data?.docs) ? data.docs : [];
942
+ const displayLabel = label ? resolveText(label) : `${formatCollectionName(collection)} by ${field}`;
943
+ const chartData = React$1.useMemo(() => {
944
+ if (!items.length) return [];
945
+ const grouped = items.reduce((acc, item) => {
946
+ const value = item[field];
947
+ if (value === void 0 || value === null) return acc;
948
+ let key;
949
+ if (value instanceof Date || !isNaN(Date.parse(value))) key = formatDateForRange(new Date(value), timeRange);
950
+ else key = String(value);
951
+ acc[key] = (acc[key] || 0) + 1;
952
+ return acc;
953
+ }, {});
954
+ return Object.entries(grouped).map(([name, value]) => ({
955
+ name,
956
+ value
957
+ })).sort((a, b) => a.name.localeCompare(b.name));
958
+ }, [
959
+ items,
960
+ field,
961
+ timeRange
962
+ ]);
963
+ const emptyContent = /* @__PURE__ */ jsx("div", {
964
+ className: "flex h-48 items-center justify-center text-muted-foreground",
965
+ children: /* @__PURE__ */ jsx("p", {
966
+ className: "text-sm",
967
+ children: "No data available"
968
+ })
969
+ });
970
+ const chartContent = chartData.length === 0 ? emptyContent : /* @__PURE__ */ jsx("div", {
971
+ className: "h-48 w-full",
972
+ children: /* @__PURE__ */ jsx(ResponsiveContainer, {
973
+ width: "100%",
974
+ height: "100%",
975
+ children: renderChart(chartType, chartData, color, showGrid)
976
+ })
977
+ });
978
+ return /* @__PURE__ */ jsx(WidgetCard, {
979
+ title: displayLabel,
980
+ isLoading,
981
+ loadingSkeleton: /* @__PURE__ */ jsx(ChartWidgetSkeleton, {}),
982
+ error: error instanceof Error ? error : error ? new Error(String(error)) : null,
983
+ onRefresh: () => refetch(),
984
+ children: chartContent
985
+ });
986
+ }
987
+ /**
988
+ * Render the appropriate chart type
989
+ */
990
+ function renderChart(type, data, color, showGrid) {
991
+ const commonProps = {
992
+ data,
993
+ margin: {
994
+ top: 5,
995
+ right: 5,
996
+ left: -20,
997
+ bottom: 5
998
+ }
999
+ };
1000
+ const axisStyle = {
1001
+ fontSize: 10,
1002
+ fill: "var(--color-muted-foreground)"
1003
+ };
1004
+ switch (type) {
1005
+ case "line": return /* @__PURE__ */ jsxs(LineChart, {
1006
+ ...commonProps,
1007
+ children: [
1008
+ showGrid && /* @__PURE__ */ jsx(CartesianGrid, {
1009
+ strokeDasharray: "3 3",
1010
+ stroke: "var(--color-border)",
1011
+ opacity: .5
1012
+ }),
1013
+ /* @__PURE__ */ jsx(XAxis, {
1014
+ dataKey: "name",
1015
+ tick: axisStyle,
1016
+ tickLine: false,
1017
+ axisLine: false
1018
+ }),
1019
+ /* @__PURE__ */ jsx(YAxis, {
1020
+ tick: axisStyle,
1021
+ tickLine: false,
1022
+ axisLine: false
1023
+ }),
1024
+ /* @__PURE__ */ jsx(Tooltip, { content: /* @__PURE__ */ jsx(ChartTooltip, {}) }),
1025
+ /* @__PURE__ */ jsx(Line, {
1026
+ type: "monotone",
1027
+ dataKey: "value",
1028
+ stroke: color,
1029
+ strokeWidth: 2,
1030
+ dot: false
1031
+ })
1032
+ ]
1033
+ });
1034
+ case "bar": return /* @__PURE__ */ jsxs(BarChart, {
1035
+ ...commonProps,
1036
+ children: [
1037
+ showGrid && /* @__PURE__ */ jsx(CartesianGrid, {
1038
+ strokeDasharray: "3 3",
1039
+ stroke: "var(--color-border)",
1040
+ opacity: .5
1041
+ }),
1042
+ /* @__PURE__ */ jsx(XAxis, {
1043
+ dataKey: "name",
1044
+ tick: axisStyle,
1045
+ tickLine: false,
1046
+ axisLine: false
1047
+ }),
1048
+ /* @__PURE__ */ jsx(YAxis, {
1049
+ tick: axisStyle,
1050
+ tickLine: false,
1051
+ axisLine: false
1052
+ }),
1053
+ /* @__PURE__ */ jsx(Tooltip, {
1054
+ content: /* @__PURE__ */ jsx(ChartTooltip, {}),
1055
+ cursor: {
1056
+ fill: "var(--color-muted)",
1057
+ opacity: .3
1058
+ }
1059
+ }),
1060
+ /* @__PURE__ */ jsx(Bar, {
1061
+ dataKey: "value",
1062
+ fill: color,
1063
+ radius: [
1064
+ 2,
1065
+ 2,
1066
+ 0,
1067
+ 0
1068
+ ]
1069
+ })
1070
+ ]
1071
+ });
1072
+ case "pie": return /* @__PURE__ */ jsxs(PieChart, { children: [/* @__PURE__ */ jsx(Pie, {
1073
+ data,
1074
+ dataKey: "value",
1075
+ nameKey: "name",
1076
+ cx: "50%",
1077
+ cy: "50%",
1078
+ outerRadius: 60,
1079
+ label: ({ name }) => name,
1080
+ labelLine: false,
1081
+ stroke: "var(--color-background)",
1082
+ strokeWidth: 2,
1083
+ children: data.map((entry) => /* @__PURE__ */ jsx(Cell, { fill: CHART_COLORS[data.indexOf(entry) % CHART_COLORS.length] }, `cell-${entry.name}`))
1084
+ }), /* @__PURE__ */ jsx(Tooltip, { content: /* @__PURE__ */ jsx(ChartTooltip, {}) })] });
1085
+ case "area":
1086
+ default: return /* @__PURE__ */ jsxs(AreaChart, {
1087
+ ...commonProps,
1088
+ children: [
1089
+ showGrid && /* @__PURE__ */ jsx(CartesianGrid, {
1090
+ strokeDasharray: "3 3",
1091
+ stroke: "var(--color-border)",
1092
+ opacity: .5
1093
+ }),
1094
+ /* @__PURE__ */ jsx(XAxis, {
1095
+ dataKey: "name",
1096
+ tick: axisStyle,
1097
+ tickLine: false,
1098
+ axisLine: false
1099
+ }),
1100
+ /* @__PURE__ */ jsx(YAxis, {
1101
+ tick: axisStyle,
1102
+ tickLine: false,
1103
+ axisLine: false
1104
+ }),
1105
+ /* @__PURE__ */ jsx(Tooltip, { content: /* @__PURE__ */ jsx(ChartTooltip, {}) }),
1106
+ /* @__PURE__ */ jsx(Area, {
1107
+ type: "monotone",
1108
+ dataKey: "value",
1109
+ stroke: color,
1110
+ fill: color,
1111
+ fillOpacity: .15
1112
+ })
1113
+ ]
1114
+ });
1115
+ }
1116
+ }
1117
+ /**
1118
+ * Format date based on time range for grouping
1119
+ */
1120
+ function formatDateForRange(date, timeRange) {
1121
+ const months = [
1122
+ "Jan",
1123
+ "Feb",
1124
+ "Mar",
1125
+ "Apr",
1126
+ "May",
1127
+ "Jun",
1128
+ "Jul",
1129
+ "Aug",
1130
+ "Sep",
1131
+ "Oct",
1132
+ "Nov",
1133
+ "Dec"
1134
+ ];
1135
+ switch (timeRange) {
1136
+ case "7d": return `${[
1137
+ "Sun",
1138
+ "Mon",
1139
+ "Tue",
1140
+ "Wed",
1141
+ "Thu",
1142
+ "Fri",
1143
+ "Sat"
1144
+ ][date.getDay()]} ${date.getDate()}`;
1145
+ case "30d": return `${months[date.getMonth()]} ${date.getDate()}`;
1146
+ case "90d": return `W${Math.ceil(date.getDate() / 7)} ${months[date.getMonth()]}`;
1147
+ case "1y": return `${months[date.getMonth()]} ${date.getFullYear()}`;
1148
+ default: return `${months[date.getMonth()]} ${date.getDate()}`;
1149
+ }
1150
+ }
1151
+
1152
+ //#endregion
1153
+ //#region src/client/components/widgets/progress-widget.tsx
1154
+ /**
1155
+ * Progress Widget
1156
+ *
1157
+ * Displays progress towards a goal.
1158
+ * Uses WidgetCard for consistent styling.
1159
+ */
1160
+ /**
1161
+ * Progress Widget Component
1162
+ *
1163
+ * Displays a progress bar with current/target values.
1164
+ *
1165
+ * @example
1166
+ * ```tsx
1167
+ * <ProgressWidget
1168
+ * config={{
1169
+ * type: "progress",
1170
+ * id: "monthly-sales",
1171
+ * title: "Monthly Sales Goal",
1172
+ * fetchFn: async (client) => ({
1173
+ * current: 75000,
1174
+ * target: 100000,
1175
+ * label: "$75,000 / $100,000"
1176
+ * }),
1177
+ * showPercentage: true,
1178
+ * }}
1179
+ * />
1180
+ * ```
1181
+ */
1182
+ function ProgressWidget({ config }) {
1183
+ const client = useAdminStore(selectClient);
1184
+ const resolveText = useResolveText();
1185
+ const { color, showPercentage = true } = config;
1186
+ const { data, isLoading, error, refetch } = useQuery({
1187
+ queryKey: [
1188
+ "widget",
1189
+ "progress",
1190
+ config.id
1191
+ ],
1192
+ queryFn: () => config.fetchFn(client),
1193
+ refetchInterval: config.refreshInterval
1194
+ });
1195
+ const title = config.title ? resolveText(config.title) : void 0;
1196
+ const percentage = data ? Math.min(data.current / data.target * 100, 100) : 0;
1197
+ const percentageFormatted = percentage.toFixed(0);
1198
+ const getProgressColor = () => {
1199
+ if (color) return color;
1200
+ if (percentage >= 100) return "bg-green-500";
1201
+ if (percentage >= 75) return "bg-primary";
1202
+ if (percentage >= 50) return "bg-yellow-500";
1203
+ return "bg-muted-foreground";
1204
+ };
1205
+ const progressContent = data ? /* @__PURE__ */ jsxs("div", {
1206
+ className: "space-y-3",
1207
+ children: [
1208
+ /* @__PURE__ */ jsx("div", {
1209
+ className: "relative",
1210
+ children: /* @__PURE__ */ jsx("div", {
1211
+ className: "h-2 w-full overflow-hidden rounded-full bg-muted",
1212
+ children: /* @__PURE__ */ jsx("div", {
1213
+ className: cn("h-full rounded-full transition-all duration-500", getProgressColor()),
1214
+ style: { width: `${percentage}%` }
1215
+ })
1216
+ })
1217
+ }),
1218
+ /* @__PURE__ */ jsxs("div", {
1219
+ className: "flex items-center justify-between text-sm",
1220
+ children: [/* @__PURE__ */ jsx("span", {
1221
+ className: "text-muted-foreground",
1222
+ children: data.label || `${data.current.toLocaleString()} / ${data.target.toLocaleString()}`
1223
+ }), showPercentage && /* @__PURE__ */ jsxs("span", {
1224
+ className: "font-medium",
1225
+ children: [percentageFormatted, "%"]
1226
+ })]
1227
+ }),
1228
+ data.subtitle && /* @__PURE__ */ jsx("p", {
1229
+ className: "text-xs text-muted-foreground",
1230
+ children: data.subtitle
1231
+ })
1232
+ ]
1233
+ }) : null;
1234
+ return /* @__PURE__ */ jsx(WidgetCard, {
1235
+ title,
1236
+ isLoading,
1237
+ loadingSkeleton: /* @__PURE__ */ jsx(ProgressWidgetSkeleton, {}),
1238
+ error: error instanceof Error ? error : error ? new Error(String(error)) : null,
1239
+ onRefresh: () => refetch(),
1240
+ children: progressContent
1241
+ });
1242
+ }
1243
+
1244
+ //#endregion
1245
+ //#region src/client/components/widgets/quick-actions-widget.tsx
1246
+ /**
1247
+ * Quick Actions Widget
1248
+ *
1249
+ * Displays shortcuts to common actions with icons and proper styling.
1250
+ * Uses WidgetCard for consistent styling.
1251
+ */
1252
+ /**
1253
+ * Quick actions widget component
1254
+ *
1255
+ * Displays a list of action items with icons, matching the style
1256
+ * of other dashboard widgets like recent-items.
1257
+ */
1258
+ function QuickActionsWidget({ config, basePath = "/admin", navigate }) {
1259
+ const resolveText = useResolveText();
1260
+ const { quickActions, layout = "list" } = config;
1261
+ const title = config.title ? resolveText(config.title) : "Quick Actions";
1262
+ const parsedActions = quickActions.map((action, index) => {
1263
+ if (typeof action === "string") {
1264
+ const [collection, actionType] = action.split(".");
1265
+ return {
1266
+ id: `${action}-${index}`,
1267
+ label: `${actionType === "create" ? "New" : actionType} ${formatCollectionName(collection)}`,
1268
+ href: `${basePath}/collections/${collection}/${actionType === "create" ? "create" : ""}`,
1269
+ icon: void 0,
1270
+ variant: "default"
1271
+ };
1272
+ }
1273
+ return {
1274
+ id: `action-${index}`,
1275
+ label: resolveText(action.label),
1276
+ href: action.href,
1277
+ onClick: action.onClick,
1278
+ icon: action.icon,
1279
+ variant: action.variant || "default"
1280
+ };
1281
+ });
1282
+ const handleClick = (action) => {
1283
+ if (action.onClick) action.onClick();
1284
+ else if (action.href && navigate) navigate(action.href);
1285
+ };
1286
+ const variantStyles$3 = {
1287
+ default: "hover:bg-muted/50 cursor-pointer",
1288
+ primary: "bg-primary/5 hover:bg-primary/10 border-primary/20 text-primary [&_svg]:text-primary cursor-pointer",
1289
+ secondary: "hover:bg-secondary cursor-pointer",
1290
+ outline: "border border-border hover:bg-muted/50 cursor-pointer"
1291
+ };
1292
+ const iconVariantStyles = {
1293
+ default: "bg-muted text-muted-foreground",
1294
+ primary: "bg-primary/10 text-primary",
1295
+ secondary: "bg-secondary text-secondary-foreground",
1296
+ outline: "bg-background text-muted-foreground"
1297
+ };
1298
+ if (parsedActions.length === 0) return /* @__PURE__ */ jsx(WidgetCard, {
1299
+ title,
1300
+ children: /* @__PURE__ */ jsx("p", {
1301
+ className: "text-sm text-muted-foreground",
1302
+ children: "No actions configured"
1303
+ })
1304
+ });
1305
+ if (layout === "grid") return /* @__PURE__ */ jsx(WidgetCard, {
1306
+ title,
1307
+ children: /* @__PURE__ */ jsx("div", {
1308
+ className: "grid grid-cols-2 gap-2",
1309
+ children: parsedActions.map((action) => {
1310
+ const Icon = action.icon && typeof action.icon !== "string" ? action.icon : null;
1311
+ return /* @__PURE__ */ jsxs("button", {
1312
+ type: "button",
1313
+ onClick: () => handleClick(action),
1314
+ className: cn("flex flex-col items-center justify-center gap-2 rounded-md p-3 text-center transition-colors", variantStyles$3[action.variant]),
1315
+ children: [Icon && /* @__PURE__ */ jsx("div", {
1316
+ className: cn("flex h-9 w-9 items-center justify-center rounded-md", iconVariantStyles[action.variant]),
1317
+ children: /* @__PURE__ */ jsx(Icon, { className: "h-4 w-4" })
1318
+ }), /* @__PURE__ */ jsx("span", {
1319
+ className: "text-xs font-medium",
1320
+ children: action.label
1321
+ })]
1322
+ }, action.id);
1323
+ })
1324
+ })
1325
+ });
1326
+ return /* @__PURE__ */ jsx(WidgetCard, {
1327
+ title,
1328
+ children: /* @__PURE__ */ jsx("div", {
1329
+ className: "space-y-1 -mx-1",
1330
+ children: parsedActions.map((action) => {
1331
+ const Icon = action.icon && typeof action.icon !== "string" ? action.icon : null;
1332
+ return /* @__PURE__ */ jsxs("button", {
1333
+ type: "button",
1334
+ onClick: () => handleClick(action),
1335
+ className: cn("flex w-full items-center gap-3 rounded-md px-2 py-2 text-left transition-colors", variantStyles$3[action.variant]),
1336
+ children: [
1337
+ Icon && /* @__PURE__ */ jsx("div", {
1338
+ className: cn("flex h-8 w-8 shrink-0 items-center justify-center rounded-md", iconVariantStyles[action.variant]),
1339
+ children: /* @__PURE__ */ jsx(Icon, { className: "h-4 w-4" })
1340
+ }),
1341
+ /* @__PURE__ */ jsx("span", {
1342
+ className: "flex-1 text-sm font-medium truncate",
1343
+ children: action.label
1344
+ }),
1345
+ /* @__PURE__ */ jsx(ArrowRight, { className: "h-4 w-4 text-muted-foreground opacity-0 group-hover:opacity-100 transition-opacity" })
1346
+ ]
1347
+ }, action.id);
1348
+ })
1349
+ })
1350
+ });
1351
+ }
1352
+
1353
+ //#endregion
1354
+ //#region src/client/components/widgets/recent-items-widget.tsx
1355
+ /**
1356
+ * Recent Items Widget
1357
+ *
1358
+ * Displays latest items from a collection.
1359
+ * Uses WidgetCard for consistent styling.
1360
+ */
1361
+ /**
1362
+ * Recent items widget component
1363
+ *
1364
+ * Displays:
1365
+ * - List of most recently created/updated items
1366
+ * - Configurable number of items
1367
+ * - Links to edit each item
1368
+ */
1369
+ function RecentItemsWidget({ config }) {
1370
+ const resolveText = useResolveText();
1371
+ const { collection, limit = 5, label, titleField = "_title", dateField = "createdAt", subtitleFields, onItemClick } = config;
1372
+ const { data, isLoading, error, refetch } = useCollectionList(collection, {
1373
+ orderBy: { [dateField]: "desc" },
1374
+ limit
1375
+ });
1376
+ const items = Array.isArray(data?.docs) ? data.docs : [];
1377
+ const displayLabel = label ? resolveText(label) : `Recent ${formatCollectionName(collection)}`;
1378
+ const getTitleValue = (item) => {
1379
+ if (titleField && item[titleField]) return String(item[titleField]);
1380
+ if (item._title) return String(item._title);
1381
+ if (item.name) return String(item.name);
1382
+ if (item.title) return String(item.title);
1383
+ if (item.label) return String(item.label);
1384
+ return `Item #${item.id}`;
1385
+ };
1386
+ const getSubtitle = (item) => {
1387
+ if (!subtitleFields?.length) return null;
1388
+ return subtitleFields.map((field) => item[field]).filter(Boolean).join(" - ");
1389
+ };
1390
+ const handleItemClick = (item) => {
1391
+ if (onItemClick) onItemClick(item);
1392
+ };
1393
+ const listContent = items.length === 0 ? /* @__PURE__ */ jsx("p", {
1394
+ className: "text-sm text-muted-foreground",
1395
+ children: "No items yet"
1396
+ }) : /* @__PURE__ */ jsx("div", {
1397
+ className: "space-y-1",
1398
+ children: items.map((item) => {
1399
+ const dateValue = item[dateField];
1400
+ const subtitle = getSubtitle(item);
1401
+ return /* @__PURE__ */ jsx("button", {
1402
+ type: "button",
1403
+ onClick: () => handleItemClick(item),
1404
+ className: "flex w-full items-center gap-3 rounded-md p-2 hover:bg-muted/50 cursor-pointer transition-colors text-left",
1405
+ children: /* @__PURE__ */ jsxs("div", {
1406
+ className: "flex-1 min-w-0",
1407
+ children: [/* @__PURE__ */ jsx("p", {
1408
+ className: "text-sm font-medium truncate",
1409
+ children: getTitleValue(item)
1410
+ }), /* @__PURE__ */ jsxs("div", {
1411
+ className: "flex items-center gap-2 text-xs text-muted-foreground",
1412
+ children: [dateValue && /* @__PURE__ */ jsx("span", { children: formatRelativeTime(new Date(dateValue)) }), subtitle && /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx("span", { children: "-" }), /* @__PURE__ */ jsx("span", {
1413
+ className: "truncate",
1414
+ children: subtitle
1415
+ })] })]
1416
+ })]
1417
+ })
1418
+ }, item.id);
1419
+ })
1420
+ });
1421
+ return /* @__PURE__ */ jsx(WidgetCard, {
1422
+ title: displayLabel,
1423
+ isLoading,
1424
+ loadingSkeleton: /* @__PURE__ */ jsx(RecentItemsWidgetSkeleton, { count: limit }),
1425
+ error: error instanceof Error ? error : error ? new Error(String(error)) : null,
1426
+ onRefresh: () => refetch(),
1427
+ children: listContent
1428
+ });
1429
+ }
1430
+ /**
1431
+ * Format relative time
1432
+ */
1433
+ function formatRelativeTime(date) {
1434
+ const diff = (/* @__PURE__ */ new Date()).getTime() - date.getTime();
1435
+ const seconds = Math.floor(diff / 1e3);
1436
+ const minutes = Math.floor(seconds / 60);
1437
+ const hours = Math.floor(minutes / 60);
1438
+ const days = Math.floor(hours / 24);
1439
+ if (days > 0) return `${days}d ago`;
1440
+ if (hours > 0) return `${hours}h ago`;
1441
+ if (minutes > 0) return `${minutes}m ago`;
1442
+ return "just now";
1443
+ }
1444
+
1445
+ //#endregion
1446
+ //#region src/client/components/widgets/stats-widget.tsx
1447
+ /**
1448
+ * Get date range for a preset
1449
+ */
1450
+ function getDateRange(preset) {
1451
+ const now = /* @__PURE__ */ new Date();
1452
+ const today = new Date(now.getFullYear(), now.getMonth(), now.getDate());
1453
+ switch (preset) {
1454
+ case "today": return {
1455
+ gte: today,
1456
+ lte: /* @__PURE__ */ new Date(today.getTime() + 1440 * 60 * 1e3 - 1)
1457
+ };
1458
+ case "yesterday": return {
1459
+ gte: /* @__PURE__ */ new Date(today.getTime() - 1440 * 60 * 1e3),
1460
+ lte: /* @__PURE__ */ new Date(today.getTime() - 1)
1461
+ };
1462
+ case "thisWeek": {
1463
+ const dayOfWeek = today.getDay();
1464
+ return {
1465
+ gte: /* @__PURE__ */ new Date(today.getTime() - (dayOfWeek === 0 ? 6 : dayOfWeek - 1) * 24 * 60 * 60 * 1e3),
1466
+ lte: now
1467
+ };
1468
+ }
1469
+ case "lastWeek": {
1470
+ const dayOfWeek = today.getDay();
1471
+ const thisMonday = /* @__PURE__ */ new Date(today.getTime() - (dayOfWeek === 0 ? 6 : dayOfWeek - 1) * 24 * 60 * 60 * 1e3);
1472
+ return {
1473
+ gte: /* @__PURE__ */ new Date(thisMonday.getTime() - 10080 * 60 * 1e3),
1474
+ lte: /* @__PURE__ */ new Date(thisMonday.getTime() - 1)
1475
+ };
1476
+ }
1477
+ case "thisMonth": return {
1478
+ gte: new Date(now.getFullYear(), now.getMonth(), 1),
1479
+ lte: now
1480
+ };
1481
+ case "lastMonth": {
1482
+ const firstOfThisMonth = new Date(now.getFullYear(), now.getMonth(), 1);
1483
+ return {
1484
+ gte: new Date(now.getFullYear(), now.getMonth() - 1, 1),
1485
+ lte: /* @__PURE__ */ new Date(firstOfThisMonth.getTime() - 1)
1486
+ };
1487
+ }
1488
+ case "last7days": return {
1489
+ gte: /* @__PURE__ */ new Date(now.getTime() - 10080 * 60 * 1e3),
1490
+ lte: now
1491
+ };
1492
+ case "last30days": return {
1493
+ gte: /* @__PURE__ */ new Date(now.getTime() - 720 * 60 * 60 * 1e3),
1494
+ lte: now
1495
+ };
1496
+ case "last90days": return {
1497
+ gte: /* @__PURE__ */ new Date(now.getTime() - 2160 * 60 * 60 * 1e3),
1498
+ lte: now
1499
+ };
1500
+ case "thisYear": return {
1501
+ gte: new Date(now.getFullYear(), 0, 1),
1502
+ lte: now
1503
+ };
1504
+ case "lastYear": return {
1505
+ gte: new Date(now.getFullYear() - 1, 0, 1),
1506
+ lte: new Date(now.getFullYear() - 1, 11, 31, 23, 59, 59)
1507
+ };
1508
+ default: return {
1509
+ gte: today,
1510
+ lte: now
1511
+ };
1512
+ }
1513
+ }
1514
+ const variantStyles$1 = {
1515
+ default: "",
1516
+ primary: "border-primary/30 bg-primary/5",
1517
+ success: "border-green-500/30 bg-green-500/5",
1518
+ warning: "border-yellow-500/30 bg-yellow-500/5",
1519
+ danger: "border-red-500/30 bg-red-500/5"
1520
+ };
1521
+ const variantValueStyles = {
1522
+ default: "",
1523
+ primary: "text-primary",
1524
+ success: "text-green-600 dark:text-green-400",
1525
+ warning: "text-yellow-600 dark:text-yellow-400",
1526
+ danger: "text-red-600 dark:text-red-400"
1527
+ };
1528
+ /**
1529
+ * Stats widget component
1530
+ *
1531
+ * Shows:
1532
+ * - Total count for a collection
1533
+ * - Optional icon
1534
+ * - Color variant for visual distinction
1535
+ */
1536
+ function StatsWidget({ config }) {
1537
+ const resolveText = useResolveText();
1538
+ const { collection, label, filter, filterFn, dateFilter, icon: Icon, variant = "default" } = config;
1539
+ const computedFilter = useMemo(() => {
1540
+ let result = {};
1541
+ if (filter) result = {
1542
+ ...result,
1543
+ ...filter
1544
+ };
1545
+ if (filterFn) result = {
1546
+ ...result,
1547
+ ...filterFn()
1548
+ };
1549
+ if (dateFilter) {
1550
+ const { gte, lte } = getDateRange(dateFilter.range);
1551
+ result[dateFilter.field] = {
1552
+ gte: gte.toISOString(),
1553
+ lte: lte.toISOString()
1554
+ };
1555
+ }
1556
+ return Object.keys(result).length > 0 ? result : void 0;
1557
+ }, [
1558
+ filter,
1559
+ filterFn,
1560
+ dateFilter
1561
+ ]);
1562
+ const { data: count = 0, isLoading, error, refetch } = useCollectionCount(collection, computedFilter ? { where: computedFilter } : void 0);
1563
+ return /* @__PURE__ */ jsx(WidgetCard, {
1564
+ title: label ? resolveText(label) : formatCollectionName(collection),
1565
+ icon: Icon && typeof Icon !== "string" ? Icon : void 0,
1566
+ isLoading,
1567
+ loadingSkeleton: /* @__PURE__ */ jsx(StatsWidgetSkeleton, {}),
1568
+ error: error instanceof Error ? error : error ? new Error(String(error)) : null,
1569
+ onRefresh: () => refetch(),
1570
+ className: variantStyles$1[variant],
1571
+ children: /* @__PURE__ */ jsx("div", {
1572
+ className: cn("text-2xl font-bold", variantValueStyles[variant]),
1573
+ children: count.toLocaleString()
1574
+ })
1575
+ });
1576
+ }
1577
+
1578
+ //#endregion
1579
+ //#region src/client/components/widgets/table-widget.tsx
1580
+ /**
1581
+ * Field types that need fieldDef passed to their cell component
1582
+ */
1583
+ const FIELD_TYPES_NEEDING_FIELD_DEF = new Set([
1584
+ "object",
1585
+ "array",
1586
+ "relation",
1587
+ "reverseRelation"
1588
+ ]);
1589
+ /**
1590
+ * Normalize column config - converts string to object format
1591
+ */
1592
+ function normalizeColumn(column) {
1593
+ if (typeof column === "string") return { key: column };
1594
+ return column;
1595
+ }
1596
+ /**
1597
+ * Get column label from field definition or column config
1598
+ */
1599
+ function getColumnLabel(column, fieldDef) {
1600
+ if (column.label) return column.label;
1601
+ const fieldOptions = fieldDef?.["~options"];
1602
+ if (fieldOptions?.label) return fieldOptions.label;
1603
+ return column.key.replace(/([A-Z])/g, " $1").replace(/^./, (s) => s.toUpperCase()).trim();
1604
+ }
1605
+ /**
1606
+ * Resolve cell component from field definition
1607
+ */
1608
+ function resolveCellComponent(fieldDef) {
1609
+ if (fieldDef?.cell?.component) return fieldDef.cell.component;
1610
+ return DefaultCell;
1611
+ }
1612
+ /**
1613
+ * Table Widget Component
1614
+ *
1615
+ * Displays a mini table of collection items.
1616
+ * Uses field definitions from admin config for labels and cell rendering.
1617
+ */
1618
+ function TableWidget({ config, basePath = "/admin", navigate }) {
1619
+ const resolveText = useResolveText();
1620
+ const admin = useAdminStore(selectAdmin);
1621
+ const { collection, columns: rawColumns, limit = 5, sortBy, sortOrder = "desc", filter, linkToDetail, emptyMessage } = config;
1622
+ const columns = rawColumns.map(normalizeColumn);
1623
+ const fields = (admin?.getCollections() ?? {})[collection]?.fields;
1624
+ const queryOptions = { limit };
1625
+ if (sortBy) queryOptions.orderBy = { [sortBy]: sortOrder };
1626
+ if (filter) queryOptions.where = filter;
1627
+ const { data, isLoading, error, refetch } = useCollectionList(collection, queryOptions);
1628
+ const items = Array.isArray(data?.docs) ? data.docs : [];
1629
+ const title = config.title ? resolveText(config.title) : void 0;
1630
+ const handleRowClick = (item) => {
1631
+ if (linkToDetail && navigate) navigate(`${basePath}/collections/${collection}/${item.id}`);
1632
+ };
1633
+ const renderCell = (item, column) => {
1634
+ const value = item[column.key];
1635
+ if (column.render) return column.render(value, item);
1636
+ const fieldDef = fields?.[column.key];
1637
+ const CellComponent = resolveCellComponent(fieldDef);
1638
+ const fieldType = fieldDef?.name ?? "text";
1639
+ return FIELD_TYPES_NEEDING_FIELD_DEF.has(fieldType) ? /* @__PURE__ */ jsx(CellComponent, {
1640
+ value,
1641
+ row: item,
1642
+ fieldDef
1643
+ }) : /* @__PURE__ */ jsx(CellComponent, {
1644
+ value,
1645
+ row: item
1646
+ });
1647
+ };
1648
+ const emptyContent = /* @__PURE__ */ jsx("div", {
1649
+ className: "flex h-24 items-center justify-center text-muted-foreground",
1650
+ children: /* @__PURE__ */ jsx("p", {
1651
+ className: "text-sm",
1652
+ children: emptyMessage ? resolveText(emptyMessage) : "No data available"
1653
+ })
1654
+ });
1655
+ const tableContent = items.length === 0 ? emptyContent : /* @__PURE__ */ jsxs("div", {
1656
+ className: "-mx-5 -mb-1",
1657
+ children: [/* @__PURE__ */ jsx("div", {
1658
+ className: "flex items-center gap-2 px-5 py-2 border-b border-border/30 text-[10px] font-medium uppercase tracking-wider text-muted-foreground bg-muted/20 backdrop-blur-sm",
1659
+ children: columns.map((column) => {
1660
+ const fieldDef = fields?.[column.key];
1661
+ const label = getColumnLabel(column, fieldDef);
1662
+ return /* @__PURE__ */ jsx("div", {
1663
+ className: cn("flex-1 min-w-0", column.align === "center" && "text-center", column.align === "right" && "text-right"),
1664
+ style: column.width ? {
1665
+ width: column.width,
1666
+ flex: "none"
1667
+ } : void 0,
1668
+ children: resolveText(label)
1669
+ }, column.key);
1670
+ })
1671
+ }), items.map((item) => linkToDetail ? /* @__PURE__ */ jsx("button", {
1672
+ type: "button",
1673
+ className: "flex w-full items-center gap-2 px-5 py-2.5 border-b border-border/20 last:border-0 transition-all cursor-pointer hover:bg-muted/30 hover:backdrop-blur-sm text-left",
1674
+ onClick: () => handleRowClick(item),
1675
+ children: columns.map((column) => /* @__PURE__ */ jsx("div", {
1676
+ className: cn("flex-1 min-w-0 text-sm truncate", column.align === "center" && "text-center", column.align === "right" && "text-right"),
1677
+ style: column.width ? {
1678
+ width: column.width,
1679
+ flex: "none"
1680
+ } : void 0,
1681
+ children: renderCell(item, column)
1682
+ }, column.key))
1683
+ }, item.id) : /* @__PURE__ */ jsx("div", {
1684
+ className: "flex items-center gap-2 px-5 py-2.5 border-b border-border/20 last:border-0",
1685
+ children: columns.map((column) => /* @__PURE__ */ jsx("div", {
1686
+ className: cn("flex-1 min-w-0 text-sm truncate", column.align === "center" && "text-center", column.align === "right" && "text-right"),
1687
+ style: column.width ? {
1688
+ width: column.width,
1689
+ flex: "none"
1690
+ } : void 0,
1691
+ children: renderCell(item, column)
1692
+ }, column.key))
1693
+ }, item.id))]
1694
+ });
1695
+ return /* @__PURE__ */ jsx(WidgetCard, {
1696
+ title,
1697
+ isLoading,
1698
+ loadingSkeleton: /* @__PURE__ */ jsx(TableWidgetSkeleton, {
1699
+ rows: limit,
1700
+ columns: columns.length
1701
+ }),
1702
+ error: error instanceof Error ? error : error ? new Error(String(error)) : null,
1703
+ onRefresh: () => refetch(),
1704
+ children: tableContent
1705
+ });
1706
+ }
1707
+
1708
+ //#endregion
1709
+ //#region src/client/components/widgets/timeline-widget.tsx
1710
+ /**
1711
+ * Timeline Widget
1712
+ *
1713
+ * Displays an activity/event timeline.
1714
+ * Uses WidgetCard for consistent styling.
1715
+ */
1716
+ const variantStyles = {
1717
+ default: "bg-muted-foreground",
1718
+ success: "bg-green-500",
1719
+ warning: "bg-yellow-500",
1720
+ error: "bg-red-500",
1721
+ info: "bg-blue-500"
1722
+ };
1723
+ /**
1724
+ * Format timestamp based on format option
1725
+ */
1726
+ function formatTimestamp(date, format = "relative") {
1727
+ const d = typeof date === "string" ? new Date(date) : date;
1728
+ switch (format) {
1729
+ case "absolute": return d.toLocaleDateString();
1730
+ case "datetime": return d.toLocaleString();
1731
+ case "relative":
1732
+ default: {
1733
+ const diff = (/* @__PURE__ */ new Date()).getTime() - d.getTime();
1734
+ const seconds = Math.floor(diff / 1e3);
1735
+ const minutes = Math.floor(seconds / 60);
1736
+ const hours = Math.floor(minutes / 60);
1737
+ const days = Math.floor(hours / 24);
1738
+ if (days > 7) return d.toLocaleDateString();
1739
+ if (days > 0) return `${days}d ago`;
1740
+ if (hours > 0) return `${hours}h ago`;
1741
+ if (minutes > 0) return `${minutes}m ago`;
1742
+ return "just now";
1743
+ }
1744
+ }
1745
+ }
1746
+ /**
1747
+ * Timeline Widget Component
1748
+ *
1749
+ * Displays a vertical timeline of events/activities.
1750
+ *
1751
+ * @example
1752
+ * ```tsx
1753
+ * <TimelineWidget
1754
+ * config={{
1755
+ * type: "timeline",
1756
+ * id: "recent-activity",
1757
+ * title: "Recent Activity",
1758
+ * fetchFn: async (client) => {
1759
+ * const activities = await client.collections.activities.findMany({
1760
+ * limit: 10,
1761
+ * orderBy: { createdAt: "desc" }
1762
+ * });
1763
+ * return activities.map(a => ({
1764
+ * id: a.id,
1765
+ * title: a.action,
1766
+ * description: a.description,
1767
+ * timestamp: a.createdAt,
1768
+ * variant: a.type === "error" ? "error" : "default"
1769
+ * }));
1770
+ * }
1771
+ * }}
1772
+ * />
1773
+ * ```
1774
+ */
1775
+ function TimelineWidget({ config, navigate }) {
1776
+ const client = useAdminStore(selectClient);
1777
+ const resolveText = useResolveText();
1778
+ const { maxItems = 10, showTimestamps = true, timestampFormat = "relative", emptyMessage } = config;
1779
+ const { data, isLoading, error, refetch } = useQuery({
1780
+ queryKey: [
1781
+ "widget",
1782
+ "timeline",
1783
+ config.id
1784
+ ],
1785
+ queryFn: () => config.fetchFn(client),
1786
+ refetchInterval: config.refreshInterval
1787
+ });
1788
+ const items = data?.slice(0, maxItems) ?? [];
1789
+ const title = config.title ? resolveText(config.title) : void 0;
1790
+ const handleItemClick = (item) => {
1791
+ if (item.href && navigate) navigate(item.href);
1792
+ };
1793
+ const emptyContent = /* @__PURE__ */ jsx("div", {
1794
+ className: "flex h-24 items-center justify-center text-muted-foreground",
1795
+ children: /* @__PURE__ */ jsx("p", {
1796
+ className: "text-sm",
1797
+ children: emptyMessage ? resolveText(emptyMessage) : "No activity yet"
1798
+ })
1799
+ });
1800
+ const timelineContent = items.length === 0 ? emptyContent : /* @__PURE__ */ jsx("div", {
1801
+ className: "space-y-0",
1802
+ children: items.map((item, index) => {
1803
+ const Icon = item.icon || Circle;
1804
+ const variant = item.variant || "default";
1805
+ const isLast = index === items.length - 1;
1806
+ const isClickable = !!(item.href && navigate);
1807
+ const itemContent = /* @__PURE__ */ jsxs(Fragment, { children: [
1808
+ !isLast && /* @__PURE__ */ jsx("div", { className: "absolute left-[11px] top-6 bottom-0 w-px bg-border" }),
1809
+ /* @__PURE__ */ jsx("div", {
1810
+ className: cn("relative z-10 flex h-6 w-6 shrink-0 items-center justify-center rounded-full", variantStyles[variant]),
1811
+ children: /* @__PURE__ */ jsx(Icon, {
1812
+ className: "h-3 w-3 text-white",
1813
+ weight: "bold"
1814
+ })
1815
+ }),
1816
+ /* @__PURE__ */ jsxs("div", {
1817
+ className: "flex-1 min-w-0 pt-0.5 text-left",
1818
+ children: [
1819
+ /* @__PURE__ */ jsx("p", {
1820
+ className: "text-sm font-medium truncate",
1821
+ children: item.title
1822
+ }),
1823
+ item.description && /* @__PURE__ */ jsx("p", {
1824
+ className: "text-xs text-muted-foreground mt-0.5 line-clamp-2",
1825
+ children: item.description
1826
+ }),
1827
+ showTimestamps && item.timestamp && /* @__PURE__ */ jsx("p", {
1828
+ className: "text-xs text-muted-foreground mt-1",
1829
+ children: formatTimestamp(item.timestamp, timestampFormat)
1830
+ })
1831
+ ]
1832
+ })
1833
+ ] });
1834
+ if (isClickable) return /* @__PURE__ */ jsx("button", {
1835
+ type: "button",
1836
+ className: cn("relative flex gap-3 pb-4 w-full", "cursor-pointer hover:bg-muted/30 -mx-2 px-2 rounded-md transition-colors", isLast && "pb-0"),
1837
+ onClick: () => handleItemClick(item),
1838
+ children: itemContent
1839
+ }, item.id);
1840
+ return /* @__PURE__ */ jsx("div", {
1841
+ className: cn("relative flex gap-3 pb-4", isLast && "pb-0"),
1842
+ children: itemContent
1843
+ }, item.id);
1844
+ })
1845
+ });
1846
+ return /* @__PURE__ */ jsx(WidgetCard, {
1847
+ title,
1848
+ isLoading,
1849
+ loadingSkeleton: /* @__PURE__ */ jsx(TimelineWidgetSkeleton, { count: maxItems }),
1850
+ error: error instanceof Error ? error : error ? new Error(String(error)) : null,
1851
+ onRefresh: () => refetch(),
1852
+ children: timelineContent
1853
+ });
1854
+ }
1855
+
1856
+ //#endregion
1857
+ //#region src/client/components/widgets/value-widget.tsx
1858
+ /**
1859
+ * Value Widget
1860
+ *
1861
+ * Flexible widget with async data fetching and full Tailwind customization.
1862
+ * Uses WidgetCard for consistent styling while allowing custom classNames.
1863
+ */
1864
+ /**
1865
+ * Format value for display
1866
+ */
1867
+ function formatValue(value) {
1868
+ if (typeof value === "number") return value.toLocaleString();
1869
+ return value;
1870
+ }
1871
+ /**
1872
+ * Value Widget Component
1873
+ *
1874
+ * Fetches data via async function and renders with full customization.
1875
+ *
1876
+ * @example
1877
+ * ```tsx
1878
+ * <ValueWidget
1879
+ * config={{
1880
+ * type: "value",
1881
+ * id: "total-revenue",
1882
+ * fetchFn: async (client) => ({
1883
+ * value: 42,
1884
+ * label: "Total",
1885
+ * classNames: { root: "bg-blue-50" },
1886
+ * }),
1887
+ * }}
1888
+ * />
1889
+ * ```
1890
+ */
1891
+ function ValueWidget({ config }) {
1892
+ const client = useAdminStore(selectClient);
1893
+ const resolveText = useResolveText();
1894
+ const { data, isLoading, error, refetch, isFetching } = useQuery({
1895
+ queryKey: [
1896
+ "widget",
1897
+ "value",
1898
+ config.id
1899
+ ],
1900
+ queryFn: () => config.fetchFn(client),
1901
+ refetchInterval: config.refreshInterval
1902
+ });
1903
+ const isFeatured = config.cardVariant === "featured";
1904
+ if (isLoading || error || !data) return /* @__PURE__ */ jsx(WidgetCard, {
1905
+ title: config.title ? resolveText(config.title) : void 0,
1906
+ isLoading,
1907
+ loadingSkeleton: /* @__PURE__ */ jsx(ValueWidgetSkeleton, { featured: isFeatured }),
1908
+ error: error instanceof Error ? error : !data && !isLoading ? /* @__PURE__ */ new Error("No data returned") : null,
1909
+ onRefresh: () => refetch(),
1910
+ className: data?.classNames?.root
1911
+ });
1912
+ const cls = data.classNames ?? {};
1913
+ const Icon = data.icon;
1914
+ const TrendIcon = data.trend?.icon;
1915
+ const label = data.label ? resolveText(data.label) : void 0;
1916
+ const subtitle = data.subtitle ? resolveText(data.subtitle) : void 0;
1917
+ const footer = data.footer ? resolveText(data.footer) : void 0;
1918
+ return /* @__PURE__ */ jsx(WidgetCard, {
1919
+ title: label,
1920
+ icon: Icon,
1921
+ onRefresh: () => refetch(),
1922
+ isRefreshing: isFetching && !isLoading,
1923
+ className: cls.root,
1924
+ children: /* @__PURE__ */ jsxs("div", {
1925
+ className: cn("space-y-1", cls.content),
1926
+ children: [
1927
+ /* @__PURE__ */ jsx("div", {
1928
+ className: cn("text-2xl font-bold", cls.value),
1929
+ children: data.formatted ?? formatValue(data.value)
1930
+ }),
1931
+ data.trend && /* @__PURE__ */ jsxs("div", {
1932
+ className: cn("flex items-center gap-1 text-sm", cls.trend),
1933
+ children: [TrendIcon && /* @__PURE__ */ jsx(TrendIcon, { className: cn("h-3 w-3", cls.trendIcon) }), /* @__PURE__ */ jsx("span", { children: data.trend.value })]
1934
+ }),
1935
+ subtitle && /* @__PURE__ */ jsx("p", {
1936
+ className: cn("text-xs text-muted-foreground", cls.subtitle),
1937
+ children: subtitle
1938
+ }),
1939
+ footer && /* @__PURE__ */ jsx("p", {
1940
+ className: cn("text-xs text-muted-foreground pt-2", cls.footer),
1941
+ children: footer
1942
+ })
1943
+ ]
1944
+ })
1945
+ });
1946
+ }
1947
+
1948
+ //#endregion
1949
+ //#region src/client/views/dashboard/dashboard-widget.tsx
1950
+ /**
1951
+ * DashboardWidget Component
1952
+ *
1953
+ * Renders individual dashboard widgets based on their type configuration.
1954
+ * Supports built-in widget types and custom components.
1955
+ */
1956
+ function UnknownWidget({ type }) {
1957
+ return /* @__PURE__ */ jsx(WidgetCard, {
1958
+ title: "Unknown Widget",
1959
+ className: "border-yellow-500/20 bg-yellow-500/5",
1960
+ children: /* @__PURE__ */ jsxs("p", {
1961
+ className: "text-xs text-muted-foreground",
1962
+ children: [
1963
+ "Widget type \"",
1964
+ type,
1965
+ "\" is not recognized."
1966
+ ]
1967
+ })
1968
+ });
1969
+ }
1970
+ function CustomWidgetRenderer({ loader, widgetConfig, span }) {
1971
+ const [state, setState] = React$1.useState({
1972
+ Component: null,
1973
+ loading: true,
1974
+ error: null
1975
+ });
1976
+ React$1.useEffect(() => {
1977
+ if (!loader) {
1978
+ setState({
1979
+ Component: null,
1980
+ loading: false,
1981
+ error: null
1982
+ });
1983
+ return;
1984
+ }
1985
+ if (!(typeof loader === "function" && !loader.prototype?.render && !loader.prototype?.isReactComponent && loader.length === 0)) {
1986
+ setState({
1987
+ Component: loader,
1988
+ loading: false,
1989
+ error: null
1990
+ });
1991
+ return;
1992
+ }
1993
+ let mounted = true;
1994
+ (async () => {
1995
+ try {
1996
+ const result = await loader();
1997
+ if (mounted) setState({
1998
+ Component: result.default || result,
1999
+ loading: false,
2000
+ error: null
2001
+ });
2002
+ } catch (err) {
2003
+ if (mounted) setState({
2004
+ Component: null,
2005
+ loading: false,
2006
+ error: err instanceof Error ? err : /* @__PURE__ */ new Error("Failed to load component")
2007
+ });
2008
+ }
2009
+ })();
2010
+ return () => {
2011
+ mounted = false;
2012
+ };
2013
+ }, [loader]);
2014
+ if (state.loading) return /* @__PURE__ */ jsx(WidgetCard, { isLoading: true });
2015
+ if (state.error) return /* @__PURE__ */ jsx(WidgetCard, { error: state.error });
2016
+ if (!state.Component) return /* @__PURE__ */ jsx(WidgetCard, { error: /* @__PURE__ */ new Error("Component not found") });
2017
+ const Component = state.Component;
2018
+ return /* @__PURE__ */ jsx(Component, {
2019
+ config: widgetConfig,
2020
+ span
2021
+ });
2022
+ }
2023
+ function StatsWidgetRenderer({ config }) {
2024
+ const resolveText = useResolveText();
2025
+ return /* @__PURE__ */ jsx(StatsWidget, { config: {
2026
+ collection: config.collection,
2027
+ label: config.label ? resolveText(config.label) : void 0,
2028
+ filter: config.filter,
2029
+ filterFn: config.filterFn,
2030
+ dateFilter: config.dateFilter,
2031
+ icon: config.icon,
2032
+ variant: config.variant
2033
+ } });
2034
+ }
2035
+ function ChartWidgetRenderer({ config }) {
2036
+ const resolveText = useResolveText();
2037
+ return /* @__PURE__ */ jsx(ChartWidget, { config: {
2038
+ collection: config.collection,
2039
+ field: config.field,
2040
+ chartType: config.chartType,
2041
+ timeRange: config.timeRange,
2042
+ label: config.label ? resolveText(config.label) : void 0,
2043
+ color: config.color,
2044
+ showGrid: config.showGrid,
2045
+ aggregation: config.aggregation,
2046
+ valueField: config.valueField
2047
+ } });
2048
+ }
2049
+ function RecentItemsWidgetRenderer({ config, basePath, navigate }) {
2050
+ const resolveText = useResolveText();
2051
+ return /* @__PURE__ */ jsx(RecentItemsWidget, { config: {
2052
+ collection: config.collection,
2053
+ limit: config.limit,
2054
+ label: config.label ? resolveText(config.label) : void 0,
2055
+ titleField: config.titleField,
2056
+ dateField: config.dateField,
2057
+ subtitleFields: config.subtitleFields,
2058
+ basePath,
2059
+ onItemClick: navigate ? (item) => navigate(`${basePath}/collections/${config.collection}/${item.id}`) : void 0
2060
+ } });
2061
+ }
2062
+ function QuickActionsWidgetRenderer({ config, basePath, navigate }) {
2063
+ return /* @__PURE__ */ jsx(QuickActionsWidget, {
2064
+ config,
2065
+ basePath,
2066
+ navigate
2067
+ });
2068
+ }
2069
+ function TableWidgetRenderer({ config, basePath, navigate }) {
2070
+ return /* @__PURE__ */ jsx(TableWidget, {
2071
+ config,
2072
+ basePath,
2073
+ navigate
2074
+ });
2075
+ }
2076
+ function TimelineWidgetRenderer({ config, navigate }) {
2077
+ return /* @__PURE__ */ jsx(TimelineWidget, {
2078
+ config,
2079
+ navigate
2080
+ });
2081
+ }
2082
+ function ProgressWidgetRenderer({ config }) {
2083
+ return /* @__PURE__ */ jsx(ProgressWidget, { config });
2084
+ }
2085
+ /**
2086
+ * DashboardWidget - Renders a single widget based on its configuration
2087
+ *
2088
+ * @example
2089
+ * ```tsx
2090
+ * <DashboardWidget
2091
+ * config={{ type: "stats", collection: "posts", label: "Total Posts" }}
2092
+ * basePath="/admin"
2093
+ * navigate={navigate}
2094
+ * />
2095
+ * ```
2096
+ */
2097
+ function DashboardWidget({ config, basePath = "/admin", navigate, widgetRegistry }) {
2098
+ const renderWidget = () => {
2099
+ if (config.type === "custom") return /* @__PURE__ */ jsx(CustomWidgetRenderer, {
2100
+ loader: config.component,
2101
+ widgetConfig: config.config || {},
2102
+ span: config.span
2103
+ });
2104
+ if (widgetRegistry?.[config.type]) {
2105
+ const CustomWidget = widgetRegistry[config.type];
2106
+ return /* @__PURE__ */ jsx(CustomWidget, {
2107
+ config,
2108
+ span: config.span
2109
+ });
2110
+ }
2111
+ switch (config.type) {
2112
+ case "stats": return /* @__PURE__ */ jsx(StatsWidgetRenderer, { config });
2113
+ case "chart": return /* @__PURE__ */ jsx(ChartWidgetRenderer, { config });
2114
+ case "recentItems": return /* @__PURE__ */ jsx(RecentItemsWidgetRenderer, {
2115
+ config,
2116
+ basePath,
2117
+ navigate
2118
+ });
2119
+ case "quickActions": return /* @__PURE__ */ jsx(QuickActionsWidgetRenderer, {
2120
+ config,
2121
+ basePath,
2122
+ navigate
2123
+ });
2124
+ case "value": return /* @__PURE__ */ jsx(ValueWidget, { config });
2125
+ case "table": return /* @__PURE__ */ jsx(TableWidgetRenderer, {
2126
+ config,
2127
+ basePath,
2128
+ navigate
2129
+ });
2130
+ case "timeline": return /* @__PURE__ */ jsx(TimelineWidgetRenderer, {
2131
+ config,
2132
+ navigate
2133
+ });
2134
+ case "progress": return /* @__PURE__ */ jsx(ProgressWidgetRenderer, { config });
2135
+ default: return /* @__PURE__ */ jsx(UnknownWidget, { type: config.type });
2136
+ }
2137
+ };
2138
+ return /* @__PURE__ */ jsx(WidgetErrorBoundary, {
2139
+ widgetType: config.type,
2140
+ children: renderWidget()
2141
+ });
2142
+ }
2143
+
2144
+ //#endregion
2145
+ //#region src/client/views/dashboard/dashboard-grid.tsx
2146
+ /**
2147
+ * Grid column classes for different column counts
2148
+ * Uses container queries (@container) for responsive behavior
2149
+ */
2150
+ const gridClasses = {
2151
+ 1: "grid-cols-1",
2152
+ 2: "grid-cols-1 @xs:grid-cols-2",
2153
+ 3: "grid-cols-1 @xs:grid-cols-2 @md:grid-cols-3",
2154
+ 4: "grid-cols-1 @xs:grid-cols-2 @md:grid-cols-3 @lg:grid-cols-4",
2155
+ 5: "grid-cols-1 @xs:grid-cols-2 @sm:grid-cols-3 @md:grid-cols-4 @lg:grid-cols-5",
2156
+ 6: "grid-cols-1 @xs:grid-cols-2 @sm:grid-cols-3 @md:grid-cols-4 @lg:grid-cols-5 @xl:grid-cols-6",
2157
+ 12: "grid-cols-1 @xs:grid-cols-2 @sm:grid-cols-4 @md:grid-cols-6 @lg:grid-cols-12"
2158
+ };
2159
+ /**
2160
+ * Span classes for widget column spanning
2161
+ */
2162
+ const spanClasses = {
2163
+ 1: "col-span-1",
2164
+ 2: "col-span-1 @xs:col-span-2",
2165
+ 3: "col-span-1 @xs:col-span-2 @md:col-span-3",
2166
+ 4: "col-span-1 @xs:col-span-2 @md:col-span-3 @lg:col-span-4",
2167
+ 5: "col-span-1 @xs:col-span-2 @md:col-span-3 @lg:col-span-5",
2168
+ 6: "col-span-1 @xs:col-span-2 @md:col-span-3 @lg:col-span-6",
2169
+ 12: "col-span-full"
2170
+ };
2171
+ /**
2172
+ * Get grid columns class based on column count
2173
+ */
2174
+ function getGridClass(columns) {
2175
+ return gridClasses[columns] || gridClasses[4];
2176
+ }
2177
+ /**
2178
+ * Get column span class for a widget
2179
+ */
2180
+ function getSpanClass(span) {
2181
+ if (!span || span <= 1) return "";
2182
+ return spanClasses[span] || "";
2183
+ }
2184
+ /**
2185
+ * Check if item is a widget config
2186
+ */
2187
+ function isWidgetConfig(item) {
2188
+ return typeof item === "object" && "type" in item && item.type !== "section" && item.type !== "tabs";
2189
+ }
2190
+ /**
2191
+ * Check if item is a section config
2192
+ */
2193
+ function isSectionConfig(item) {
2194
+ return typeof item === "object" && "type" in item && item.type === "section";
2195
+ }
2196
+ /**
2197
+ * Check if item is a tabs config
2198
+ */
2199
+ function isTabsConfig(item) {
2200
+ return typeof item === "object" && "type" in item && item.type === "tabs";
2201
+ }
2202
+ /**
2203
+ * Generate a unique key for a layout item
2204
+ */
2205
+ function getLayoutItemKey(item, index) {
2206
+ if (isWidgetConfig(item)) return `widget-${item.id || item.type}-${index}`;
2207
+ if (isSectionConfig(item)) return `section-${index}`;
2208
+ if (isTabsConfig(item)) return `tabs-${item.tabs.map((t) => t.id).join("-") || index}`;
2209
+ return `item-${index}`;
2210
+ }
2211
+ function DashboardHeader({ title, description, actions, navigate, resolveText }) {
2212
+ if (!title && !description && !actions?.length) return null;
2213
+ const handleActionClick = (action) => {
2214
+ if (action.onClick) action.onClick();
2215
+ else if (action.href && navigate) navigate(action.href);
2216
+ };
2217
+ return /* @__PURE__ */ jsxs("div", {
2218
+ className: "mb-8 flex flex-col gap-4 sm:flex-row sm:items-start sm:justify-between",
2219
+ children: [/* @__PURE__ */ jsxs("div", {
2220
+ className: "min-w-0 flex-1",
2221
+ children: [title && /* @__PURE__ */ jsx("h1", {
2222
+ className: "text-2xl md:text-3xl font-extrabold tracking-tight",
2223
+ children: title
2224
+ }), description && /* @__PURE__ */ jsx("p", {
2225
+ className: "mt-1 text-muted-foreground",
2226
+ children: description
2227
+ })]
2228
+ }), actions && actions.length > 0 && /* @__PURE__ */ jsx("div", {
2229
+ className: "flex items-center gap-2 shrink-0",
2230
+ children: actions.map((action) => {
2231
+ const Icon = action.icon && typeof action.icon !== "string" ? action.icon : null;
2232
+ const variant = action.variant || "default";
2233
+ return /* @__PURE__ */ jsxs(Button, {
2234
+ variant: variant === "primary" ? "default" : variant,
2235
+ size: "sm",
2236
+ onClick: () => handleActionClick(action),
2237
+ children: [Icon && /* @__PURE__ */ jsx(Icon, { className: "h-4 w-4 mr-2" }), resolveText(action.label)]
2238
+ }, action.id);
2239
+ })
2240
+ })]
2241
+ });
2242
+ }
2243
+ function LayoutItemRenderer({ item, index, columns, basePath, navigate, widgetRegistry, resolveText }) {
2244
+ if (isWidgetConfig(item)) return /* @__PURE__ */ jsx("div", {
2245
+ className: cn("min-h-0 h-full", getSpanClass(item.span)),
2246
+ children: /* @__PURE__ */ jsx(DashboardWidget, {
2247
+ config: item,
2248
+ basePath,
2249
+ navigate,
2250
+ widgetRegistry
2251
+ })
2252
+ });
2253
+ if (isSectionConfig(item)) return /* @__PURE__ */ jsx(SectionRenderer, {
2254
+ section: item,
2255
+ basePath,
2256
+ navigate,
2257
+ widgetRegistry,
2258
+ resolveText
2259
+ }, `section-${index}`);
2260
+ if (isTabsConfig(item)) return /* @__PURE__ */ jsx(TabsRenderer, {
2261
+ tabs: item,
2262
+ basePath,
2263
+ navigate,
2264
+ widgetRegistry,
2265
+ resolveText
2266
+ }, `tabs-${index}`);
2267
+ return null;
2268
+ }
2269
+ function SectionRenderer({ section, basePath, navigate, widgetRegistry, resolveText }) {
2270
+ const { label, description, wrapper = "flat", defaultCollapsed = false, layout = "grid", columns = 4, gap, items, className } = section;
2271
+ const sectionLabel = label ? resolveText(label) : void 0;
2272
+ const sectionDescription = description ? resolveText(description) : void 0;
2273
+ const itemsContent = /* @__PURE__ */ jsx("div", {
2274
+ className: cn("@container", layout === "grid" && "grid gap-4 items-stretch", layout === "grid" && getGridClass(columns), layout === "stack" && "flex flex-col gap-4"),
2275
+ style: gap ? { gap: `${gap * .25}rem` } : void 0,
2276
+ children: items.map((item, index) => /* @__PURE__ */ jsx(LayoutItemRenderer, {
2277
+ item,
2278
+ index,
2279
+ columns,
2280
+ basePath,
2281
+ navigate,
2282
+ widgetRegistry,
2283
+ resolveText
2284
+ }, getLayoutItemKey(item, index)))
2285
+ });
2286
+ if (wrapper === "flat") return /* @__PURE__ */ jsxs("div", {
2287
+ className: cn("col-span-full", className),
2288
+ children: [(sectionLabel || sectionDescription) && /* @__PURE__ */ jsxs("div", {
2289
+ className: "mb-4",
2290
+ children: [sectionLabel && /* @__PURE__ */ jsx("h2", {
2291
+ className: "text-lg font-semibold",
2292
+ children: sectionLabel
2293
+ }), sectionDescription && /* @__PURE__ */ jsx("p", {
2294
+ className: "text-sm text-muted-foreground mt-1",
2295
+ children: sectionDescription
2296
+ })]
2297
+ }), itemsContent]
2298
+ });
2299
+ if (wrapper === "card") return /* @__PURE__ */ jsxs(Card, {
2300
+ className: cn("col-span-full", className),
2301
+ children: [(sectionLabel || sectionDescription) && /* @__PURE__ */ jsxs(CardHeader, { children: [sectionLabel && /* @__PURE__ */ jsx(CardTitle, { children: sectionLabel }), sectionDescription && /* @__PURE__ */ jsx("p", {
2302
+ className: "text-sm text-muted-foreground",
2303
+ children: sectionDescription
2304
+ })] }), /* @__PURE__ */ jsx(CardContent, { children: itemsContent })]
2305
+ });
2306
+ if (wrapper === "collapsible") return /* @__PURE__ */ jsx(Accordion$1, {
2307
+ defaultValue: defaultCollapsed ? [] : [0],
2308
+ className: cn("col-span-full", className),
2309
+ children: /* @__PURE__ */ jsxs(AccordionItem, {
2310
+ className: "border-none",
2311
+ children: [/* @__PURE__ */ jsx(AccordionTrigger, {
2312
+ className: "hover:no-underline py-2",
2313
+ children: /* @__PURE__ */ jsxs("div", {
2314
+ className: "text-left",
2315
+ children: [sectionLabel && /* @__PURE__ */ jsx("span", {
2316
+ className: "text-lg font-semibold",
2317
+ children: sectionLabel
2318
+ }), sectionDescription && /* @__PURE__ */ jsx("p", {
2319
+ className: "text-sm text-muted-foreground font-normal",
2320
+ children: sectionDescription
2321
+ })]
2322
+ })
2323
+ }), /* @__PURE__ */ jsx(AccordionContent, {
2324
+ className: "pt-4",
2325
+ children: itemsContent
2326
+ })]
2327
+ })
2328
+ });
2329
+ return itemsContent;
2330
+ }
2331
+ function TabsRenderer({ tabs, basePath, navigate, widgetRegistry, resolveText }) {
2332
+ const { tabs: tabConfigs, defaultTab, variant = "default" } = tabs;
2333
+ return /* @__PURE__ */ jsxs(Tabs$1, {
2334
+ defaultValue: defaultTab || tabConfigs[0]?.id,
2335
+ className: "col-span-full",
2336
+ children: [/* @__PURE__ */ jsx(TabsList, {
2337
+ variant: variant === "line" ? "line" : "default",
2338
+ className: "mb-4",
2339
+ children: tabConfigs.map((tab) => /* @__PURE__ */ jsxs(TabsTrigger, {
2340
+ value: tab.id,
2341
+ children: [
2342
+ tab.icon && /* @__PURE__ */ jsx(tab.icon, { className: "h-4 w-4 mr-2" }),
2343
+ resolveText(tab.label),
2344
+ tab.badge !== void 0 && /* @__PURE__ */ jsx("span", {
2345
+ className: "ml-2 rounded-full bg-muted px-2 py-0.5 text-xs",
2346
+ children: tab.badge
2347
+ })
2348
+ ]
2349
+ }, tab.id))
2350
+ }), tabConfigs.map((tab) => /* @__PURE__ */ jsx(TabsContent, {
2351
+ value: tab.id,
2352
+ children: /* @__PURE__ */ jsx(TabContentRenderer, {
2353
+ tab,
2354
+ basePath,
2355
+ navigate,
2356
+ widgetRegistry,
2357
+ resolveText
2358
+ })
2359
+ }, tab.id))]
2360
+ });
2361
+ }
2362
+ function TabContentRenderer({ tab, basePath, navigate, widgetRegistry, resolveText }) {
2363
+ const columns = 4;
2364
+ return /* @__PURE__ */ jsx("div", {
2365
+ className: cn("@container grid gap-4 items-stretch", getGridClass(columns)),
2366
+ children: tab.items.map((item, index) => /* @__PURE__ */ jsx(LayoutItemRenderer, {
2367
+ item,
2368
+ index,
2369
+ columns,
2370
+ basePath,
2371
+ navigate,
2372
+ widgetRegistry,
2373
+ resolveText
2374
+ }, getLayoutItemKey(item, index)))
2375
+ });
2376
+ }
2377
+ /**
2378
+ * DashboardGrid - Renders a configurable grid of dashboard widgets
2379
+ *
2380
+ * @example
2381
+ * ```tsx
2382
+ * const dashboardConfig: DashboardConfig = {
2383
+ * title: "Dashboard",
2384
+ * description: "Welcome to your admin dashboard",
2385
+ * columns: 4,
2386
+ * items: [
2387
+ * { type: "stats", collection: "posts", label: "Total Posts", span: 1 },
2388
+ * { type: "stats", collection: "users", label: "Total Users", span: 1 },
2389
+ * {
2390
+ * type: "section",
2391
+ * label: "Analytics",
2392
+ * wrapper: "card",
2393
+ * items: [
2394
+ * { type: "chart", collection: "posts", field: "createdAt", span: 2 },
2395
+ * ]
2396
+ * },
2397
+ * {
2398
+ * type: "tabs",
2399
+ * tabs: [
2400
+ * { id: "recent", label: "Recent", items: [...] },
2401
+ * { id: "popular", label: "Popular", items: [...] },
2402
+ * ]
2403
+ * }
2404
+ * ],
2405
+ * };
2406
+ *
2407
+ * <DashboardGrid config={dashboardConfig} basePath="/admin" navigate={navigate} />
2408
+ * ```
2409
+ */
2410
+ function DashboardGrid({ config, basePath = "/admin", navigate, widgetRegistry, className }) {
2411
+ const resolveText = useResolveText();
2412
+ const { title, description, columns = 4 } = config;
2413
+ const layoutItems = config.items || config.widgets || [];
2414
+ const resolvedTitle = title ? resolveText(title) : void 0;
2415
+ const resolvedDescription = description ? resolveText(description) : void 0;
2416
+ if (layoutItems.length === 0) return /* @__PURE__ */ jsxs("div", {
2417
+ className: cn("@container", className),
2418
+ children: [/* @__PURE__ */ jsx(DashboardHeader, {
2419
+ title: resolvedTitle,
2420
+ description: resolvedDescription,
2421
+ actions: config.actions,
2422
+ navigate,
2423
+ resolveText
2424
+ }), /* @__PURE__ */ jsx("div", {
2425
+ className: "flex h-64 items-center justify-center border border-dashed border-border bg-card/50 rounded-lg",
2426
+ children: /* @__PURE__ */ jsxs("div", {
2427
+ className: "text-center",
2428
+ children: [/* @__PURE__ */ jsx("p", {
2429
+ className: "text-muted-foreground font-medium",
2430
+ children: "No widgets configured"
2431
+ }), /* @__PURE__ */ jsx("p", {
2432
+ className: "mt-1 text-sm text-muted-foreground",
2433
+ children: "Add widgets to your dashboard configuration to display data here."
2434
+ })]
2435
+ })
2436
+ })]
2437
+ });
2438
+ return /* @__PURE__ */ jsxs("div", {
2439
+ className: cn("@container", className),
2440
+ children: [/* @__PURE__ */ jsx(DashboardHeader, {
2441
+ title: resolvedTitle,
2442
+ description: resolvedDescription,
2443
+ actions: config.actions,
2444
+ navigate,
2445
+ resolveText
2446
+ }), /* @__PURE__ */ jsx("div", {
2447
+ className: cn("grid gap-4 items-stretch", getGridClass(columns)),
2448
+ children: layoutItems.map((item, index) => /* @__PURE__ */ jsx(LayoutItemRenderer, {
2449
+ item,
2450
+ index,
2451
+ columns,
2452
+ basePath,
2453
+ navigate,
2454
+ widgetRegistry,
2455
+ resolveText
2456
+ }, getLayoutItemKey(item, index)))
2457
+ })]
2458
+ });
2459
+ }
2460
+
2461
+ //#endregion
2462
+ //#region src/client/views/pages/dashboard-page.tsx
2463
+ /**
2464
+ * Default dashboard page component.
2465
+ *
2466
+ * Reads dashboard configuration from AdminProvider and renders DashboardGrid.
2467
+ *
2468
+ * @example
2469
+ * ```tsx
2470
+ * // In your admin config
2471
+ * const admin = qa<AppCMS>()
2472
+ * .use(coreAdminModule)
2473
+ * .dashboard({
2474
+ * title: "Welcome",
2475
+ * widgets: [
2476
+ * { type: "stats", collection: "posts" },
2477
+ * ],
2478
+ * })
2479
+ * ```
2480
+ */
2481
+ function DashboardPage({ title, description, className }) {
2482
+ const admin = useAdminStore(selectAdmin);
2483
+ const basePath = useAdminStore(selectBasePath);
2484
+ const navigate = useAdminStore(selectNavigate);
2485
+ return /* @__PURE__ */ jsx(DashboardGrid, {
2486
+ config: {
2487
+ ...admin.getDashboard(),
2488
+ ...title && { title },
2489
+ ...description && { description }
2490
+ },
2491
+ basePath,
2492
+ navigate,
2493
+ className
2494
+ });
2495
+ }
2496
+ var dashboard_page_default = DashboardPage;
2497
+
2498
+ //#endregion
2499
+ export { EmailCell as C, TextCell as D, SelectCell as E, TimeCell as O, DefaultCell as S, RichTextCell as T, AccordionItem as _, Skeleton as a, DateCell as b, useCollectionItem as c, Tabs$1 as d, TabsContent as f, AccordionContent as g, Accordion$1 as h, DashboardWidget as i, Badge as k, useCollectionList as l, TabsTrigger as m, dashboard_page_default as n, useCollectionCreate as o, TabsList as p, DashboardGrid as r, useCollectionDelete as s, DashboardPage as t, useCollectionUpdate as u, AccordionTrigger as v, NumberCell as w, DateTimeCell as x, BooleanCell as y };
2500
+ //# sourceMappingURL=dashboard-page-B4PGEdc2.mjs.map