@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.
- package/README.md +439 -424
- package/dist/auth-layout-M8K8_q5R.mjs +181 -0
- package/dist/auth-layout-M8K8_q5R.mjs.map +1 -0
- package/dist/bulk-upload-dialog-h7zXD78Y.mjs +274 -0
- package/dist/bulk-upload-dialog-h7zXD78Y.mjs.map +1 -0
- package/dist/{components/ui/card.mjs → card-BKHjBQfw.mjs} +8 -8
- package/dist/card-BKHjBQfw.mjs.map +1 -0
- package/dist/client/styles/index.css +434 -0
- package/dist/client-BCGpkAz6.mjs +22635 -0
- package/dist/client-BCGpkAz6.mjs.map +1 -0
- package/dist/client-CcWZbkBP.d.mts +13585 -0
- package/dist/client-CcWZbkBP.d.mts.map +1 -0
- package/dist/client.d.mts +3 -0
- package/dist/client.mjs +14 -0
- package/dist/content-locales-provider-BXvuIgfg.mjs +1650 -0
- package/dist/content-locales-provider-BXvuIgfg.mjs.map +1 -0
- package/dist/dashboard-page-B4PGEdc2.mjs +2500 -0
- package/dist/dashboard-page-B4PGEdc2.mjs.map +1 -0
- package/dist/dashboard-page-CVlyR40m.mjs +6 -0
- package/dist/dropzone-Do3awXKd.mjs +634 -0
- package/dist/dropzone-Do3awXKd.mjs.map +1 -0
- package/dist/{views/auth/forgot-password-form.mjs → forgot-password-page-Bcp-An4Y.mjs} +87 -14
- package/dist/forgot-password-page-Bcp-An4Y.mjs.map +1 -0
- package/dist/forgot-password-page-CIILVhfo.mjs +7 -0
- package/dist/index-B9Xwk4hi.d.mts +2753 -0
- package/dist/index-B9Xwk4hi.d.mts.map +1 -0
- package/dist/index.d.mts +3 -0
- package/dist/index.mjs +14 -0
- package/dist/login-page-8K7fo0qK.mjs +7 -0
- package/dist/login-page-CP4gA-dl.mjs +298 -0
- package/dist/login-page-CP4gA-dl.mjs.map +1 -0
- package/dist/preview-utils-BKQ9-TMa.mjs +65 -0
- package/dist/preview-utils-BKQ9-TMa.mjs.map +1 -0
- package/dist/{views/auth/reset-password-form.mjs → reset-password-page-BqfDmLxA.mjs} +111 -14
- package/dist/reset-password-page-BqfDmLxA.mjs.map +1 -0
- package/dist/reset-password-page-DLATv0xQ.mjs +7 -0
- package/dist/runtime-6VZM878K.mjs +69 -0
- package/dist/runtime-6VZM878K.mjs.map +1 -0
- package/dist/saved-views.types-BMsz5mCy.d.mts +42 -0
- package/dist/saved-views.types-BMsz5mCy.d.mts.map +1 -0
- package/dist/server.d.mts +250 -0
- package/dist/server.d.mts.map +1 -0
- package/dist/server.mjs +832 -0
- package/dist/server.mjs.map +1 -0
- package/dist/setup-page-CMZ5P_OE.mjs +6 -0
- package/dist/setup-page-YAP_fzqh.mjs +264 -0
- package/dist/setup-page-YAP_fzqh.mjs.map +1 -0
- package/dist/shared.d.mts +57 -0
- package/dist/shared.d.mts.map +1 -0
- package/dist/shared.mjs +3 -0
- package/dist/{hooks/use-auth.mjs → use-auth-BoLmWtmU.mjs} +42 -30
- package/dist/use-auth-BoLmWtmU.mjs.map +1 -0
- package/package.json +48 -197
- package/.turbo/turbo-build.log +0 -108
- package/CHANGELOG.md +0 -10
- package/STATUS.md +0 -917
- package/VALIDATION.md +0 -602
- package/components.json +0 -24
- package/dist/__tests__/setup.mjs +0 -38
- package/dist/__tests__/test-utils.mjs +0 -45
- package/dist/__tests__/vitest.d.mjs +0 -3
- package/dist/components/admin-app.mjs +0 -69
- package/dist/components/fields/array-field.mjs +0 -190
- package/dist/components/fields/checkbox-field.mjs +0 -34
- package/dist/components/fields/custom-field.mjs +0 -32
- package/dist/components/fields/date-field.mjs +0 -41
- package/dist/components/fields/datetime-field.mjs +0 -42
- package/dist/components/fields/email-field.mjs +0 -37
- package/dist/components/fields/embedded-collection.mjs +0 -253
- package/dist/components/fields/field-types.mjs +0 -1
- package/dist/components/fields/field-utils.mjs +0 -10
- package/dist/components/fields/field-wrapper.mjs +0 -34
- package/dist/components/fields/index.mjs +0 -23
- package/dist/components/fields/json-field.mjs +0 -243
- package/dist/components/fields/locale-badge.mjs +0 -16
- package/dist/components/fields/number-field.mjs +0 -39
- package/dist/components/fields/password-field.mjs +0 -37
- package/dist/components/fields/relation-field.mjs +0 -104
- package/dist/components/fields/relation-picker.mjs +0 -229
- package/dist/components/fields/relation-select.mjs +0 -188
- package/dist/components/fields/rich-text-editor/index.mjs +0 -897
- package/dist/components/fields/select-field.mjs +0 -41
- package/dist/components/fields/switch-field.mjs +0 -34
- package/dist/components/fields/text-field.mjs +0 -38
- package/dist/components/fields/textarea-field.mjs +0 -38
- package/dist/components/index.mjs +0 -59
- package/dist/components/primitives/checkbox-input.mjs +0 -127
- package/dist/components/primitives/date-input.mjs +0 -303
- package/dist/components/primitives/index.mjs +0 -12
- package/dist/components/primitives/number-input.mjs +0 -104
- package/dist/components/primitives/select-input.mjs +0 -177
- package/dist/components/primitives/tag-input.mjs +0 -135
- package/dist/components/primitives/text-input.mjs +0 -39
- package/dist/components/primitives/textarea-input.mjs +0 -37
- package/dist/components/primitives/toggle-input.mjs +0 -31
- package/dist/components/primitives/types.mjs +0 -12
- package/dist/components/ui/accordion.mjs +0 -55
- package/dist/components/ui/avatar.mjs +0 -54
- package/dist/components/ui/badge.mjs +0 -34
- package/dist/components/ui/button.mjs +0 -48
- package/dist/components/ui/checkbox.mjs +0 -21
- package/dist/components/ui/combobox.mjs +0 -163
- package/dist/components/ui/dialog.mjs +0 -95
- package/dist/components/ui/dropdown-menu.mjs +0 -138
- package/dist/components/ui/field.mjs +0 -113
- package/dist/components/ui/input-group.mjs +0 -82
- package/dist/components/ui/input.mjs +0 -17
- package/dist/components/ui/label.mjs +0 -15
- package/dist/components/ui/popover.mjs +0 -56
- package/dist/components/ui/scroll-area.mjs +0 -38
- package/dist/components/ui/select.mjs +0 -100
- package/dist/components/ui/separator.mjs +0 -16
- package/dist/components/ui/sheet.mjs +0 -90
- package/dist/components/ui/sidebar.mjs +0 -387
- package/dist/components/ui/skeleton.mjs +0 -14
- package/dist/components/ui/spinner.mjs +0 -16
- package/dist/components/ui/switch.mjs +0 -22
- package/dist/components/ui/table.mjs +0 -68
- package/dist/components/ui/tabs.mjs +0 -48
- package/dist/components/ui/textarea.mjs +0 -15
- package/dist/components/ui/tooltip.mjs +0 -44
- package/dist/config/component-registry.mjs +0 -38
- package/dist/config/index.mjs +0 -129
- package/dist/hooks/admin-provider.mjs +0 -70
- package/dist/hooks/index.mjs +0 -7
- package/dist/hooks/store.mjs +0 -178
- package/dist/hooks/use-collection-db.mjs +0 -146
- package/dist/hooks/use-collection.mjs +0 -112
- package/dist/hooks/use-global.mjs +0 -46
- package/dist/hooks/use-mobile.mjs +0 -20
- package/dist/lib/utils.mjs +0 -10
- package/dist/styles/index.css +0 -336
- package/dist/styles/index.mjs +0 -1
- package/dist/utils/index.mjs +0 -9
- package/dist/views/auth/auth-layout.mjs +0 -52
- package/dist/views/auth/index.mjs +0 -6
- package/dist/views/auth/login-form.mjs +0 -156
- package/dist/views/collection/auto-form-fields.mjs +0 -525
- package/dist/views/collection/collection-form.mjs +0 -91
- package/dist/views/collection/collection-list.mjs +0 -76
- package/dist/views/collection/form-field.mjs +0 -42
- package/dist/views/collection/index.mjs +0 -6
- package/dist/views/common/index.mjs +0 -4
- package/dist/views/common/locale-switcher.mjs +0 -39
- package/dist/views/common/version-history.mjs +0 -272
- package/dist/views/index.mjs +0 -9
- package/dist/views/layout/admin-layout.mjs +0 -40
- package/dist/views/layout/admin-router.mjs +0 -95
- package/dist/views/layout/admin-sidebar.mjs +0 -63
- package/dist/views/layout/index.mjs +0 -5
- package/src/__tests__/setup.ts +0 -44
- package/src/__tests__/test-utils.tsx +0 -49
- package/src/__tests__/vitest.d.ts +0 -9
- package/src/components/admin-app.tsx +0 -221
- package/src/components/fields/array-field.tsx +0 -237
- package/src/components/fields/checkbox-field.tsx +0 -47
- package/src/components/fields/custom-field.tsx +0 -50
- package/src/components/fields/date-field.tsx +0 -65
- package/src/components/fields/datetime-field.tsx +0 -67
- package/src/components/fields/email-field.tsx +0 -51
- package/src/components/fields/embedded-collection.tsx +0 -315
- package/src/components/fields/field-types.ts +0 -162
- package/src/components/fields/field-utils.ts +0 -6
- package/src/components/fields/field-wrapper.tsx +0 -52
- package/src/components/fields/index.ts +0 -66
- package/src/components/fields/json-field.tsx +0 -440
- package/src/components/fields/locale-badge.tsx +0 -15
- package/src/components/fields/number-field.tsx +0 -57
- package/src/components/fields/password-field.tsx +0 -51
- package/src/components/fields/relation-field.tsx +0 -243
- package/src/components/fields/relation-picker.tsx +0 -402
- package/src/components/fields/relation-select.tsx +0 -327
- package/src/components/fields/rich-text-editor/index.tsx +0 -1337
- package/src/components/fields/select-field.tsx +0 -61
- package/src/components/fields/switch-field.tsx +0 -47
- package/src/components/fields/text-field.tsx +0 -55
- package/src/components/fields/textarea-field.tsx +0 -55
- package/src/components/index.ts +0 -40
- package/src/components/primitives/checkbox-input.tsx +0 -193
- package/src/components/primitives/date-input.tsx +0 -401
- package/src/components/primitives/index.ts +0 -24
- package/src/components/primitives/number-input.tsx +0 -132
- package/src/components/primitives/select-input.tsx +0 -296
- package/src/components/primitives/tag-input.tsx +0 -200
- package/src/components/primitives/text-input.tsx +0 -49
- package/src/components/primitives/textarea-input.tsx +0 -46
- package/src/components/primitives/toggle-input.tsx +0 -36
- package/src/components/primitives/types.ts +0 -235
- package/src/components/ui/accordion.tsx +0 -72
- package/src/components/ui/avatar.tsx +0 -106
- package/src/components/ui/badge.tsx +0 -48
- package/src/components/ui/button.tsx +0 -53
- package/src/components/ui/card.tsx +0 -94
- package/src/components/ui/checkbox.tsx +0 -27
- package/src/components/ui/combobox.tsx +0 -290
- package/src/components/ui/dialog.tsx +0 -151
- package/src/components/ui/dropdown-menu.tsx +0 -254
- package/src/components/ui/field.tsx +0 -227
- package/src/components/ui/input-group.tsx +0 -149
- package/src/components/ui/input.tsx +0 -20
- package/src/components/ui/label.tsx +0 -18
- package/src/components/ui/popover.tsx +0 -88
- package/src/components/ui/scroll-area.tsx +0 -53
- package/src/components/ui/select.tsx +0 -192
- package/src/components/ui/separator.tsx +0 -23
- package/src/components/ui/sheet.tsx +0 -127
- package/src/components/ui/sidebar.tsx +0 -723
- package/src/components/ui/skeleton.tsx +0 -13
- package/src/components/ui/spinner.tsx +0 -10
- package/src/components/ui/switch.tsx +0 -32
- package/src/components/ui/table.tsx +0 -99
- package/src/components/ui/tabs.tsx +0 -82
- package/src/components/ui/textarea.tsx +0 -18
- package/src/components/ui/tooltip.tsx +0 -70
- package/src/config/component-registry.ts +0 -190
- package/src/config/index.ts +0 -1099
- package/src/hooks/README.md +0 -269
- package/src/hooks/admin-provider.tsx +0 -110
- package/src/hooks/index.ts +0 -41
- package/src/hooks/store.ts +0 -248
- package/src/hooks/use-auth.ts +0 -168
- package/src/hooks/use-collection-db.ts +0 -209
- package/src/hooks/use-collection.ts +0 -156
- package/src/hooks/use-global.ts +0 -69
- package/src/hooks/use-mobile.ts +0 -21
- package/src/lib/utils.ts +0 -6
- package/src/styles/index.css +0 -340
- package/src/utils/index.ts +0 -6
- package/src/views/auth/auth-layout.tsx +0 -77
- package/src/views/auth/forgot-password-form.tsx +0 -192
- package/src/views/auth/index.ts +0 -21
- package/src/views/auth/login-form.tsx +0 -229
- package/src/views/auth/reset-password-form.tsx +0 -232
- package/src/views/collection/auto-form-fields.tsx +0 -982
- package/src/views/collection/collection-form.tsx +0 -186
- package/src/views/collection/collection-list.tsx +0 -223
- package/src/views/collection/form-field.tsx +0 -52
- package/src/views/collection/index.ts +0 -15
- package/src/views/common/index.ts +0 -8
- package/src/views/common/locale-switcher.tsx +0 -45
- package/src/views/common/version-history.tsx +0 -406
- package/src/views/index.ts +0 -25
- package/src/views/layout/admin-layout.tsx +0 -117
- package/src/views/layout/admin-router.tsx +0 -206
- package/src/views/layout/admin-sidebar.tsx +0 -185
- package/src/views/layout/index.ts +0 -12
- package/tsconfig.json +0 -13
- package/tsconfig.tsbuildinfo +0 -1
- package/tsdown.config.ts +0 -13
- package/vitest.config.ts +0 -29
|
@@ -0,0 +1,1650 @@
|
|
|
1
|
+
import { DEFAULT_LOCALE, DEFAULT_LOCALE_CONFIG, validationMessagesEN } from "questpie/shared";
|
|
2
|
+
import * as React$1 from "react";
|
|
3
|
+
import { createContext, useCallback, useContext, useEffect, useRef } from "react";
|
|
4
|
+
import { jsx } from "react/jsx-runtime";
|
|
5
|
+
import { Button } from "@base-ui/react/button";
|
|
6
|
+
import { cva } from "class-variance-authority";
|
|
7
|
+
import { clsx } from "clsx";
|
|
8
|
+
import { twMerge } from "tailwind-merge";
|
|
9
|
+
import { useQuery } from "@tanstack/react-query";
|
|
10
|
+
import { createStore, useStore } from "zustand";
|
|
11
|
+
|
|
12
|
+
//#region src/client/i18n/hooks.tsx
|
|
13
|
+
/**
|
|
14
|
+
* I18n React Hooks
|
|
15
|
+
*
|
|
16
|
+
* React context and hooks for i18n in the admin UI.
|
|
17
|
+
*/
|
|
18
|
+
const I18nContext = createContext(null);
|
|
19
|
+
/**
|
|
20
|
+
* I18n Provider Component
|
|
21
|
+
*
|
|
22
|
+
* Provides i18n to all child components.
|
|
23
|
+
* The adapter is the source of truth for UI locale.
|
|
24
|
+
*
|
|
25
|
+
* @example
|
|
26
|
+
* ```tsx
|
|
27
|
+
* import { I18nProvider, createSimpleI18n } from "@questpie/admin/i18n";
|
|
28
|
+
*
|
|
29
|
+
* const i18n = createSimpleI18n({ ... });
|
|
30
|
+
*
|
|
31
|
+
* <I18nProvider adapter={i18n}>
|
|
32
|
+
* <App />
|
|
33
|
+
* </I18nProvider>
|
|
34
|
+
* ```
|
|
35
|
+
*/
|
|
36
|
+
function I18nProvider({ adapter, children }) {
|
|
37
|
+
const [, forceUpdate] = React$1.useReducer((x) => x + 1, 0);
|
|
38
|
+
React$1.useEffect(() => {
|
|
39
|
+
return adapter.onLocaleChange(() => {
|
|
40
|
+
forceUpdate();
|
|
41
|
+
});
|
|
42
|
+
}, [adapter]);
|
|
43
|
+
const contextValue = React$1.useMemo(() => ({
|
|
44
|
+
locale: adapter.locale,
|
|
45
|
+
locales: adapter.locales,
|
|
46
|
+
t: adapter.t.bind(adapter),
|
|
47
|
+
setLocale: adapter.setLocale.bind(adapter),
|
|
48
|
+
onLocaleChange: adapter.onLocaleChange.bind(adapter),
|
|
49
|
+
formatDate: adapter.formatDate.bind(adapter),
|
|
50
|
+
formatNumber: adapter.formatNumber.bind(adapter),
|
|
51
|
+
formatRelative: adapter.formatRelative?.bind(adapter),
|
|
52
|
+
getLocaleName: adapter.getLocaleName.bind(adapter),
|
|
53
|
+
isRTL: adapter.isRTL.bind(adapter)
|
|
54
|
+
}), [adapter, adapter.locale]);
|
|
55
|
+
return /* @__PURE__ */ jsx(I18nContext.Provider, {
|
|
56
|
+
value: contextValue,
|
|
57
|
+
children
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Get the i18n adapter (throws if not in provider)
|
|
62
|
+
*/
|
|
63
|
+
function useI18n() {
|
|
64
|
+
const adapter = useContext(I18nContext);
|
|
65
|
+
if (!adapter) throw new Error("useI18n must be used within I18nProvider");
|
|
66
|
+
return adapter;
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Get i18n adapter or null (safe version)
|
|
70
|
+
*/
|
|
71
|
+
function useSafeI18n() {
|
|
72
|
+
return useContext(I18nContext);
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* Main translation hook
|
|
76
|
+
*
|
|
77
|
+
* @example
|
|
78
|
+
* ```tsx
|
|
79
|
+
* function MyComponent() {
|
|
80
|
+
* const { t, locale, formatDate } = useTranslation();
|
|
81
|
+
* return <h1>{t("dashboard.title")}</h1>;
|
|
82
|
+
* }
|
|
83
|
+
* ```
|
|
84
|
+
*/
|
|
85
|
+
function useTranslation() {
|
|
86
|
+
const adapter = useI18n();
|
|
87
|
+
return {
|
|
88
|
+
locale: adapter.locale,
|
|
89
|
+
locales: adapter.locales,
|
|
90
|
+
t: adapter.t,
|
|
91
|
+
setLocale: adapter.setLocale,
|
|
92
|
+
formatDate: adapter.formatDate,
|
|
93
|
+
formatNumber: adapter.formatNumber,
|
|
94
|
+
getLocaleName: adapter.getLocaleName,
|
|
95
|
+
isRTL: adapter.isRTL()
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* Check if an object is a locale map (has locale codes as keys)
|
|
100
|
+
*/
|
|
101
|
+
function isLocaleMap(obj) {
|
|
102
|
+
if ("key" in obj) return false;
|
|
103
|
+
return Object.values(obj).every((v) => typeof v === "string");
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* Resolve locale map to string for given locale
|
|
107
|
+
*/
|
|
108
|
+
function resolveLocaleMap(map, locale, fallback) {
|
|
109
|
+
if (map[locale]) return map[locale];
|
|
110
|
+
const lang = locale.split("-")[0];
|
|
111
|
+
if (lang && map[lang]) return map[lang];
|
|
112
|
+
if (map[DEFAULT_LOCALE]) return map[DEFAULT_LOCALE];
|
|
113
|
+
return Object.values(map)[0] ?? fallback;
|
|
114
|
+
}
|
|
115
|
+
/**
|
|
116
|
+
* Resolve I18nText to string
|
|
117
|
+
*
|
|
118
|
+
* @example
|
|
119
|
+
* ```tsx
|
|
120
|
+
* function Label({ text }: { text: I18nText }) {
|
|
121
|
+
* const resolve = useResolveText();
|
|
122
|
+
* return <span>{resolve(text)}</span>;
|
|
123
|
+
* }
|
|
124
|
+
* ```
|
|
125
|
+
*/
|
|
126
|
+
function useResolveText() {
|
|
127
|
+
const adapter = useSafeI18n();
|
|
128
|
+
return useCallback((text, fallback = "", contextValues) => {
|
|
129
|
+
const resolveValue = (value) => {
|
|
130
|
+
if (value === void 0 || value === null) return fallback;
|
|
131
|
+
if (typeof value === "string") return value;
|
|
132
|
+
if (typeof value === "function") {
|
|
133
|
+
if (!adapter && !contextValues) return fallback;
|
|
134
|
+
const i18nCtx = adapter ? {
|
|
135
|
+
locale: adapter.locale,
|
|
136
|
+
t: adapter.t,
|
|
137
|
+
formatDate: adapter.formatDate,
|
|
138
|
+
formatNumber: adapter.formatNumber
|
|
139
|
+
} : void 0;
|
|
140
|
+
const ctx = {
|
|
141
|
+
...contextValues ?? {},
|
|
142
|
+
...i18nCtx ?? {}
|
|
143
|
+
};
|
|
144
|
+
try {
|
|
145
|
+
return resolveValue(value(ctx));
|
|
146
|
+
} catch (error) {
|
|
147
|
+
console.error("Failed to resolve dynamic text:", error);
|
|
148
|
+
return fallback;
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
if (typeof value === "object" && "key" in value && typeof value.key === "string") {
|
|
152
|
+
const keyObj = value;
|
|
153
|
+
if (!adapter) return keyObj.fallback ?? fallback;
|
|
154
|
+
const result = adapter.t(keyObj.key, keyObj.params);
|
|
155
|
+
return result === keyObj.key ? keyObj.fallback ?? result : result;
|
|
156
|
+
}
|
|
157
|
+
if (typeof value === "object" && isLocaleMap(value)) return resolveLocaleMap(value, adapter?.locale ?? DEFAULT_LOCALE, fallback);
|
|
158
|
+
return fallback;
|
|
159
|
+
};
|
|
160
|
+
return resolveValue(text);
|
|
161
|
+
}, [adapter]);
|
|
162
|
+
}
|
|
163
|
+
/**
|
|
164
|
+
* Resolve I18nText synchronously (without adapter)
|
|
165
|
+
* Only works with string and object types, not functions
|
|
166
|
+
*
|
|
167
|
+
* @param locale - Optional locale to use for locale maps (defaults to "en")
|
|
168
|
+
*/
|
|
169
|
+
function resolveTextSync(text, fallback = "", locale = DEFAULT_LOCALE) {
|
|
170
|
+
if (text === void 0 || text === null) return fallback;
|
|
171
|
+
if (typeof text === "string") return text;
|
|
172
|
+
if (typeof text === "function") return fallback;
|
|
173
|
+
if (typeof text === "object" && "key" in text) return text.fallback ?? fallback;
|
|
174
|
+
if (typeof text === "object" && isLocaleMap(text)) return resolveLocaleMap(text, locale, fallback);
|
|
175
|
+
return fallback;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
//#endregion
|
|
179
|
+
//#region src/client/lib/utils.ts
|
|
180
|
+
function cn(...inputs) {
|
|
181
|
+
return twMerge(clsx(inputs));
|
|
182
|
+
}
|
|
183
|
+
/**
|
|
184
|
+
* Format a collection name for display
|
|
185
|
+
*
|
|
186
|
+
* Converts camelCase/PascalCase to Title Case with spaces.
|
|
187
|
+
*
|
|
188
|
+
* @example
|
|
189
|
+
* formatCollectionName("blogPosts") // "Blog Posts"
|
|
190
|
+
* formatCollectionName("userSettings") // "User Settings"
|
|
191
|
+
*/
|
|
192
|
+
function formatCollectionName(name) {
|
|
193
|
+
return name.replace(/([A-Z])/g, " $1").replace(/^./, (s) => s.toUpperCase()).trim();
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
//#endregion
|
|
197
|
+
//#region src/client/components/ui/button.tsx
|
|
198
|
+
const buttonVariants = cva("focus-visible:border-ring cursor-pointer focus-visible:ring-ring/30 focus-visible:shadow-[0_0_10px_oklch(0.55_0.3_300_/_0.15)] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:aria-invalid:border-destructive/50 border border-transparent bg-clip-padding text-sm font-medium focus-visible:ring-[2px] aria-invalid:ring-[2px] [&_svg:not([class*='size-'])]:size-4 inline-flex items-center justify-center whitespace-nowrap transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none shrink-0 [&_svg]:shrink-0 outline-none group/button select-none", {
|
|
199
|
+
variants: {
|
|
200
|
+
variant: {
|
|
201
|
+
default: "bg-primary text-primary-foreground hover:bg-primary/90 shadow-sm hover:shadow-[0_0_15px_oklch(0.55_0.3_300_/_0.2)]",
|
|
202
|
+
outline: "border-input/80 bg-input/20 backdrop-blur-sm hover:bg-accent hover:text-foreground aria-expanded:bg-muted aria-expanded:text-foreground",
|
|
203
|
+
secondary: "bg-secondary text-secondary-foreground hover:bg-secondary/80 aria-expanded:bg-secondary aria-expanded:text-secondary-foreground",
|
|
204
|
+
ghost: "hover:bg-muted hover:text-foreground dark:hover:bg-muted/50 aria-expanded:bg-muted aria-expanded:text-foreground",
|
|
205
|
+
destructive: "bg-destructive/10 hover:bg-destructive/20 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/20 text-destructive focus-visible:border-destructive/40 dark:hover:bg-destructive/30",
|
|
206
|
+
link: "text-primary underline-offset-4 hover:underline"
|
|
207
|
+
},
|
|
208
|
+
size: {
|
|
209
|
+
default: "h-9 gap-1.5 px-3 has-data-[icon=inline-end]:pr-2.5 has-data-[icon=inline-start]:pl-2.5 [&_svg:not([class*='size-'])]:size-4",
|
|
210
|
+
xs: "h-6 gap-1 px-2 text-xs has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 [&_svg:not([class*='size-'])]:size-3",
|
|
211
|
+
sm: "h-7 gap-1 px-2.5 text-xs has-data-[icon=inline-end]:pr-2 has-data-[icon=inline-start]:pl-2 [&_svg:not([class*='size-'])]:size-3.5",
|
|
212
|
+
lg: "h-10 gap-2 px-4 has-data-[icon=inline-end]:pr-3 has-data-[icon=inline-start]:pl-3 [&_svg:not([class*='size-'])]:size-4",
|
|
213
|
+
icon: "size-9 [&_svg:not([class*='size-'])]:size-4",
|
|
214
|
+
"icon-xs": "size-6 [&_svg:not([class*='size-'])]:size-3",
|
|
215
|
+
"icon-sm": "size-7 [&_svg:not([class*='size-'])]:size-3.5",
|
|
216
|
+
"icon-lg": "size-10 [&_svg:not([class*='size-'])]:size-5"
|
|
217
|
+
}
|
|
218
|
+
},
|
|
219
|
+
defaultVariants: {
|
|
220
|
+
variant: "default",
|
|
221
|
+
size: "default"
|
|
222
|
+
}
|
|
223
|
+
});
|
|
224
|
+
function Button$1({ className, variant = "default", size = "default", ...props }) {
|
|
225
|
+
return /* @__PURE__ */ jsx(Button, {
|
|
226
|
+
"data-slot": "button",
|
|
227
|
+
className: cn(buttonVariants({
|
|
228
|
+
variant,
|
|
229
|
+
size,
|
|
230
|
+
className
|
|
231
|
+
})),
|
|
232
|
+
...props
|
|
233
|
+
});
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
//#endregion
|
|
237
|
+
//#region src/client/builder/admin.ts
|
|
238
|
+
/**
|
|
239
|
+
* Admin Runtime Class
|
|
240
|
+
*
|
|
241
|
+
* Wraps AdminBuilder state with runtime methods for accessing configuration.
|
|
242
|
+
* This is what gets passed to AdminProvider and used throughout the admin UI.
|
|
243
|
+
*/
|
|
244
|
+
/**
|
|
245
|
+
* Admin runtime instance
|
|
246
|
+
*
|
|
247
|
+
* Provides methods to access admin configuration at runtime.
|
|
248
|
+
* Can be created from an AdminBuilder directly - no need for Admin.from().
|
|
249
|
+
*
|
|
250
|
+
* @example
|
|
251
|
+
* ```ts
|
|
252
|
+
* import { Admin } from "@questpie/admin/builder";
|
|
253
|
+
*
|
|
254
|
+
* // Direct usage - pass builder to AdminLayoutProvider
|
|
255
|
+
* <AdminLayoutProvider admin={admin} ... />
|
|
256
|
+
*
|
|
257
|
+
* // Or create Admin instance explicitly if needed
|
|
258
|
+
* const adminInstance = Admin.from(adminBuilder);
|
|
259
|
+
* ```
|
|
260
|
+
*/
|
|
261
|
+
var Admin = class Admin {
|
|
262
|
+
constructor(state) {
|
|
263
|
+
this.state = state;
|
|
264
|
+
}
|
|
265
|
+
/**
|
|
266
|
+
* Create Admin from an AdminBuilder or return existing Admin instance.
|
|
267
|
+
* This is called internally by AdminLayoutProvider - you don't need to call it manually.
|
|
268
|
+
*
|
|
269
|
+
* @deprecated Pass the builder directly to AdminLayoutProvider instead
|
|
270
|
+
*/
|
|
271
|
+
static from(input) {
|
|
272
|
+
if (input instanceof Admin) return input;
|
|
273
|
+
return new Admin(input.state);
|
|
274
|
+
}
|
|
275
|
+
/**
|
|
276
|
+
* Normalize input to Admin instance (internal helper)
|
|
277
|
+
*/
|
|
278
|
+
static normalize(input) {
|
|
279
|
+
if (input instanceof Admin) return input;
|
|
280
|
+
return new Admin(input.state);
|
|
281
|
+
}
|
|
282
|
+
/**
|
|
283
|
+
* Get all collection names
|
|
284
|
+
*/
|
|
285
|
+
getCollectionNames() {
|
|
286
|
+
return Object.keys(this.state.collections ?? {});
|
|
287
|
+
}
|
|
288
|
+
/**
|
|
289
|
+
* Get all collection configurations.
|
|
290
|
+
* Automatically extracts `.state` from builders.
|
|
291
|
+
*/
|
|
292
|
+
getCollections() {
|
|
293
|
+
const collections = this.state.collections ?? {};
|
|
294
|
+
const result = {};
|
|
295
|
+
for (const [name, config] of Object.entries(collections)) result[name] = config && typeof config === "object" && "state" in config ? config.state : config;
|
|
296
|
+
return result;
|
|
297
|
+
}
|
|
298
|
+
/**
|
|
299
|
+
* Get configuration for a specific collection.
|
|
300
|
+
* Automatically extracts `.state` from builder.
|
|
301
|
+
*/
|
|
302
|
+
getCollectionConfig(name) {
|
|
303
|
+
const config = this.state.collections?.[name];
|
|
304
|
+
if (!config) return void 0;
|
|
305
|
+
return config && typeof config === "object" && "state" in config ? config.state : config;
|
|
306
|
+
}
|
|
307
|
+
/**
|
|
308
|
+
* Get all global names
|
|
309
|
+
*/
|
|
310
|
+
getGlobalNames() {
|
|
311
|
+
return Object.keys(this.state.globals ?? {});
|
|
312
|
+
}
|
|
313
|
+
/**
|
|
314
|
+
* Get all global configurations.
|
|
315
|
+
* Automatically extracts `.state` from builders.
|
|
316
|
+
*/
|
|
317
|
+
getGlobals() {
|
|
318
|
+
const globals = this.state.globals ?? {};
|
|
319
|
+
const result = {};
|
|
320
|
+
for (const [name, config] of Object.entries(globals)) result[name] = config && typeof config === "object" && "state" in config ? config.state : config;
|
|
321
|
+
return result;
|
|
322
|
+
}
|
|
323
|
+
/**
|
|
324
|
+
* Get configuration for a specific global.
|
|
325
|
+
* Automatically extracts `.state` from builder.
|
|
326
|
+
*/
|
|
327
|
+
getGlobalConfig(name) {
|
|
328
|
+
const config = this.state.globals?.[name];
|
|
329
|
+
if (!config) return void 0;
|
|
330
|
+
return config && typeof config === "object" && "state" in config ? config.state : config;
|
|
331
|
+
}
|
|
332
|
+
/**
|
|
333
|
+
* Get all custom page configurations.
|
|
334
|
+
* Automatically extracts `.state` from builders.
|
|
335
|
+
*/
|
|
336
|
+
getPages() {
|
|
337
|
+
const pages = this.state.pages ?? {};
|
|
338
|
+
const result = {};
|
|
339
|
+
for (const [name, config] of Object.entries(pages)) result[name] = config && typeof config === "object" && "state" in config ? config.state : config;
|
|
340
|
+
return result;
|
|
341
|
+
}
|
|
342
|
+
/**
|
|
343
|
+
* Get configuration for a specific page.
|
|
344
|
+
* Automatically extracts `.state` from builder.
|
|
345
|
+
*/
|
|
346
|
+
getPageConfig(name) {
|
|
347
|
+
const config = this.state.pages?.[name];
|
|
348
|
+
if (!config) return void 0;
|
|
349
|
+
return config && typeof config === "object" && "state" in config ? config.state : config;
|
|
350
|
+
}
|
|
351
|
+
/**
|
|
352
|
+
* Get dashboard configuration
|
|
353
|
+
*/
|
|
354
|
+
getDashboard() {
|
|
355
|
+
return this.state.dashboard ?? {};
|
|
356
|
+
}
|
|
357
|
+
/**
|
|
358
|
+
* Get sidebar configuration
|
|
359
|
+
*/
|
|
360
|
+
getSidebar() {
|
|
361
|
+
return this.state.sidebar ?? {};
|
|
362
|
+
}
|
|
363
|
+
/**
|
|
364
|
+
* Get branding configuration
|
|
365
|
+
*/
|
|
366
|
+
getBranding() {
|
|
367
|
+
return this.state.branding ?? {};
|
|
368
|
+
}
|
|
369
|
+
/**
|
|
370
|
+
* Get default views configuration
|
|
371
|
+
*/
|
|
372
|
+
getDefaultViews() {
|
|
373
|
+
return this.state.defaultViews ?? {};
|
|
374
|
+
}
|
|
375
|
+
/**
|
|
376
|
+
* Get locale configuration
|
|
377
|
+
*/
|
|
378
|
+
getLocale() {
|
|
379
|
+
return this.state.locale ?? DEFAULT_LOCALE_CONFIG;
|
|
380
|
+
}
|
|
381
|
+
/**
|
|
382
|
+
* Get available locales
|
|
383
|
+
*/
|
|
384
|
+
getAvailableLocales() {
|
|
385
|
+
return this.getLocale().supported ?? [DEFAULT_LOCALE];
|
|
386
|
+
}
|
|
387
|
+
/**
|
|
388
|
+
* Get default locale
|
|
389
|
+
*/
|
|
390
|
+
getDefaultLocale() {
|
|
391
|
+
return this.getLocale().default ?? DEFAULT_LOCALE;
|
|
392
|
+
}
|
|
393
|
+
/**
|
|
394
|
+
* Get human-readable label for a locale
|
|
395
|
+
*/
|
|
396
|
+
getLocaleLabel(locale) {
|
|
397
|
+
return {
|
|
398
|
+
en: "English",
|
|
399
|
+
sk: "Slovencina",
|
|
400
|
+
cs: "Cestina",
|
|
401
|
+
de: "Deutsch",
|
|
402
|
+
fr: "Francais",
|
|
403
|
+
es: "Espanol",
|
|
404
|
+
it: "Italiano",
|
|
405
|
+
pt: "Portugues",
|
|
406
|
+
pl: "Polski",
|
|
407
|
+
nl: "Nederlands",
|
|
408
|
+
ru: "Russkij",
|
|
409
|
+
uk: "Ukrainska",
|
|
410
|
+
ja: "Nihongo",
|
|
411
|
+
ko: "Hangugeo",
|
|
412
|
+
zh: "Zhongwen"
|
|
413
|
+
}[locale] ?? locale.toUpperCase();
|
|
414
|
+
}
|
|
415
|
+
/**
|
|
416
|
+
* Get translations map from builder state
|
|
417
|
+
* Used by AdminProvider to initialize i18n
|
|
418
|
+
*/
|
|
419
|
+
getTranslations() {
|
|
420
|
+
return this.state.translations ?? {};
|
|
421
|
+
}
|
|
422
|
+
/**
|
|
423
|
+
* Get all registered field definitions
|
|
424
|
+
*/
|
|
425
|
+
getFields() {
|
|
426
|
+
return this.state.fields ?? {};
|
|
427
|
+
}
|
|
428
|
+
/**
|
|
429
|
+
* Get a specific field definition
|
|
430
|
+
*/
|
|
431
|
+
getField(name) {
|
|
432
|
+
return this.state.fields?.[name];
|
|
433
|
+
}
|
|
434
|
+
/**
|
|
435
|
+
* Get all registered list view definitions
|
|
436
|
+
*/
|
|
437
|
+
getListViews() {
|
|
438
|
+
return this.state.listViews ?? {};
|
|
439
|
+
}
|
|
440
|
+
/**
|
|
441
|
+
* Get all registered edit view definitions
|
|
442
|
+
*/
|
|
443
|
+
getEditViews() {
|
|
444
|
+
return this.state.editViews ?? {};
|
|
445
|
+
}
|
|
446
|
+
/**
|
|
447
|
+
* Get all registered widget definitions
|
|
448
|
+
*/
|
|
449
|
+
getWidgets() {
|
|
450
|
+
return this.state.widgets ?? {};
|
|
451
|
+
}
|
|
452
|
+
};
|
|
453
|
+
|
|
454
|
+
//#endregion
|
|
455
|
+
//#region src/client/i18n/messages/en.ts
|
|
456
|
+
/**
|
|
457
|
+
* English Admin UI Messages (Default)
|
|
458
|
+
*/
|
|
459
|
+
const adminMessagesEN = {
|
|
460
|
+
"common.save": "Save",
|
|
461
|
+
"common.cancel": "Cancel",
|
|
462
|
+
"common.delete": "Delete",
|
|
463
|
+
"common.edit": "Edit",
|
|
464
|
+
"common.create": "Create",
|
|
465
|
+
"common.add": "Add",
|
|
466
|
+
"common.remove": "Remove",
|
|
467
|
+
"common.close": "Close",
|
|
468
|
+
"common.form": "Form",
|
|
469
|
+
"common.search": "Search",
|
|
470
|
+
"common.filter": "Filter",
|
|
471
|
+
"common.refresh": "Refresh",
|
|
472
|
+
"common.loading": "Loading...",
|
|
473
|
+
"common.confirm": "Confirm",
|
|
474
|
+
"common.back": "Back",
|
|
475
|
+
"common.next": "Next",
|
|
476
|
+
"common.previous": "Previous",
|
|
477
|
+
"common.actions": "Actions",
|
|
478
|
+
"common.more": "More",
|
|
479
|
+
"common.yes": "Yes",
|
|
480
|
+
"common.no": "No",
|
|
481
|
+
"common.ok": "OK",
|
|
482
|
+
"common.apply": "Apply",
|
|
483
|
+
"common.reset": "Reset",
|
|
484
|
+
"common.clear": "Clear",
|
|
485
|
+
"common.selectAll": "Select all",
|
|
486
|
+
"common.deselectAll": "Deselect all",
|
|
487
|
+
"common.duplicate": "Duplicate",
|
|
488
|
+
"common.copy": "Copy",
|
|
489
|
+
"common.paste": "Paste",
|
|
490
|
+
"common.upload": "Upload",
|
|
491
|
+
"common.download": "Download",
|
|
492
|
+
"common.preview": "Preview",
|
|
493
|
+
"common.view": "View",
|
|
494
|
+
"common.open": "Open",
|
|
495
|
+
"common.retry": "Retry",
|
|
496
|
+
"common.submit": "Submit",
|
|
497
|
+
"nav.dashboard": "Dashboard",
|
|
498
|
+
"nav.collections": "Collections",
|
|
499
|
+
"nav.globals": "Globals",
|
|
500
|
+
"nav.media": "Media",
|
|
501
|
+
"nav.settings": "Settings",
|
|
502
|
+
"nav.logout": "Log out",
|
|
503
|
+
"nav.home": "Home",
|
|
504
|
+
"nav.back": "Back",
|
|
505
|
+
"dashboard.title": "Dashboard",
|
|
506
|
+
"dashboard.welcome": "Welcome back",
|
|
507
|
+
"dashboard.recentActivity": "Recent Activity",
|
|
508
|
+
"dashboard.quickActions": "Quick Actions",
|
|
509
|
+
"collection.create": "Create {{name}}",
|
|
510
|
+
"collection.edit": "Edit {{name}}",
|
|
511
|
+
"collection.delete": "Delete {{name}}",
|
|
512
|
+
"collection.deleteConfirm": "Are you sure you want to delete this {{name}}?",
|
|
513
|
+
"collection.noItems": "No {{name}} found",
|
|
514
|
+
"collection.createFirst": "Create your first {{name}}",
|
|
515
|
+
"collection.itemCount": {
|
|
516
|
+
one: "{{count}} item",
|
|
517
|
+
other: "{{count}} items"
|
|
518
|
+
},
|
|
519
|
+
"collection.bulkDelete": "Delete selected",
|
|
520
|
+
"collection.bulkDeleteConfirm": "Are you sure you want to delete {{count}} items?",
|
|
521
|
+
"collection.bulkDeleteSuccess": {
|
|
522
|
+
one: "Successfully deleted {{count}} item",
|
|
523
|
+
other: "Successfully deleted {{count}} items"
|
|
524
|
+
},
|
|
525
|
+
"collection.bulkDeleteError": "Failed to delete items",
|
|
526
|
+
"collection.bulkDeletePartial": {
|
|
527
|
+
one: "Deleted {{success}} item, {{failed}} failed",
|
|
528
|
+
other: "Deleted {{success}} items, {{failed}} failed"
|
|
529
|
+
},
|
|
530
|
+
"collection.bulkActionFailed": "Bulk action failed",
|
|
531
|
+
"collection.selected": "{{count}} selected",
|
|
532
|
+
"collection.selectOnPage": "All on this page",
|
|
533
|
+
"collection.selectAllMatching": "All matching filter ({{count}})",
|
|
534
|
+
"collection.clearSelection": "Clear selection",
|
|
535
|
+
"collection.list": "{{name}} list",
|
|
536
|
+
"collection.new": "New {{name}}",
|
|
537
|
+
"collection.duplicateSuccess": "{{name}} duplicated successfully",
|
|
538
|
+
"collection.duplicateError": "Failed to duplicate {{name}}",
|
|
539
|
+
"relation.select": "Select {{name}}",
|
|
540
|
+
"relation.clear": "Clear selection",
|
|
541
|
+
"relation.search": "Search {{name}}...",
|
|
542
|
+
"relation.noResults": "No {{name}} found",
|
|
543
|
+
"relation.loading": "Loading...",
|
|
544
|
+
"relation.createNew": "Create new {{name}}",
|
|
545
|
+
"relation.selected": "{{count}} selected",
|
|
546
|
+
"relation.removeItem": "Remove {{name}}",
|
|
547
|
+
"relation.addItem": "Add {{name}}",
|
|
548
|
+
"relation.noneSelected": "No {{name}} selected",
|
|
549
|
+
"relation.noRelated": "No related items found",
|
|
550
|
+
"relation.saveFirst": "Save this item first to see related items.",
|
|
551
|
+
"array.empty": "No {{name}} added yet",
|
|
552
|
+
"array.addItem": "Add {{name}}",
|
|
553
|
+
"blocks.addAbove": "Add above",
|
|
554
|
+
"blocks.addBelow": "Add below",
|
|
555
|
+
"blocks.addChild": "Add child block",
|
|
556
|
+
"form.id": "ID",
|
|
557
|
+
"form.created": "Created",
|
|
558
|
+
"form.updated": "Updated",
|
|
559
|
+
"form.required": "This field is required",
|
|
560
|
+
"form.invalid": "Invalid value",
|
|
561
|
+
"form.saveChanges": "Save changes",
|
|
562
|
+
"form.unsavedChanges": "You have unsaved changes",
|
|
563
|
+
"form.discardChanges": "Discard changes",
|
|
564
|
+
"form.discardConfirm": "Are you sure you want to discard your changes? This action cannot be undone.",
|
|
565
|
+
"form.fieldRequired": "{{field}} is required",
|
|
566
|
+
"form.fieldInvalid": "{{field}} is invalid",
|
|
567
|
+
"form.maxLength": "Must be at most {{max}} characters",
|
|
568
|
+
"form.minLength": "Must be at least {{min}} characters",
|
|
569
|
+
"form.maxValue": "Must be at most {{max}}",
|
|
570
|
+
"form.minValue": "Must be at least {{min}}",
|
|
571
|
+
"form.pattern": "Invalid format",
|
|
572
|
+
"form.email": "Invalid email address",
|
|
573
|
+
"form.url": "Invalid URL",
|
|
574
|
+
"form.createSuccess": "{{name}} created successfully",
|
|
575
|
+
"form.createError": "Failed to create {{name}}",
|
|
576
|
+
"form.updateSuccess": "{{name}} updated successfully",
|
|
577
|
+
"form.updateError": "Failed to update {{name}}",
|
|
578
|
+
"form.deleteSuccess": "{{name}} deleted successfully",
|
|
579
|
+
"form.deleteError": "Failed to delete {{name}}",
|
|
580
|
+
"auth.login": "Log in",
|
|
581
|
+
"auth.logout": "Log out",
|
|
582
|
+
"auth.email": "Email",
|
|
583
|
+
"auth.password": "Password",
|
|
584
|
+
"auth.forgotPassword": "Forgot password?",
|
|
585
|
+
"auth.resetPassword": "Reset password",
|
|
586
|
+
"auth.signIn": "Sign in",
|
|
587
|
+
"auth.signOut": "Sign out",
|
|
588
|
+
"auth.signUp": "Sign up",
|
|
589
|
+
"auth.rememberMe": "Remember me",
|
|
590
|
+
"auth.invalidCredentials": "Invalid email or password",
|
|
591
|
+
"auth.sessionExpired": "Your session has expired. Please log in again.",
|
|
592
|
+
"auth.emailPlaceholder": "you@example.com",
|
|
593
|
+
"auth.passwordPlaceholder": "Enter your password",
|
|
594
|
+
"auth.signingIn": "Signing in...",
|
|
595
|
+
"auth.creatingAdmin": "Creating admin...",
|
|
596
|
+
"auth.name": "Name",
|
|
597
|
+
"auth.namePlaceholder": "Your name",
|
|
598
|
+
"auth.confirmPassword": "Confirm password",
|
|
599
|
+
"auth.confirmPasswordPlaceholder": "Confirm your password",
|
|
600
|
+
"auth.acceptInvite": "Accept invite",
|
|
601
|
+
"auth.acceptingInvite": "Accepting invite...",
|
|
602
|
+
"auth.dontHaveAccount": "Don't have an account?",
|
|
603
|
+
"auth.alreadyHaveAccount": "Already have an account?",
|
|
604
|
+
"auth.emailRequired": "Email is required",
|
|
605
|
+
"auth.passwordRequired": "Password is required",
|
|
606
|
+
"auth.passwordMinLength": "Password must be at least {{min}} characters",
|
|
607
|
+
"auth.nameRequired": "Name is required",
|
|
608
|
+
"auth.nameMinLength": "Name must be at least {{min}} characters",
|
|
609
|
+
"auth.invalidEmail": "Invalid email address",
|
|
610
|
+
"auth.passwordMismatch": "Passwords do not match",
|
|
611
|
+
"auth.newPassword": "New password",
|
|
612
|
+
"auth.newPasswordPlaceholder": "Enter new password",
|
|
613
|
+
"auth.sendResetLink": "Send reset link",
|
|
614
|
+
"auth.sendingResetLink": "Sending...",
|
|
615
|
+
"auth.resetLinkSent": "Password reset link sent to your email",
|
|
616
|
+
"auth.resettingPassword": "Resetting password...",
|
|
617
|
+
"auth.createFirstAdmin": "Create first admin",
|
|
618
|
+
"auth.setupTitle": "Setup",
|
|
619
|
+
"auth.setupDescription": "Create your first admin account to get started.",
|
|
620
|
+
"auth.profile": "Profile",
|
|
621
|
+
"auth.myAccount": "My account",
|
|
622
|
+
"error.notFound": "Not found",
|
|
623
|
+
"error.serverError": "Server error",
|
|
624
|
+
"error.networkError": "Network error. Please check your connection.",
|
|
625
|
+
"error.unauthorized": "You are not authorized to perform this action",
|
|
626
|
+
"error.forbidden": "Access denied",
|
|
627
|
+
"error.validation": "Validation failed",
|
|
628
|
+
"error.unknown": "An unknown error occurred",
|
|
629
|
+
"error.timeout": "Request timed out. Please try again.",
|
|
630
|
+
"error.conflict": "A conflict occurred. Please refresh and try again.",
|
|
631
|
+
"table.rowsPerPage": "Rows per page",
|
|
632
|
+
"table.of": "of",
|
|
633
|
+
"table.noResults": "No results",
|
|
634
|
+
"table.selectAll": "Select all",
|
|
635
|
+
"table.selectRow": "Select row",
|
|
636
|
+
"table.showing": "Showing {{from}} to {{to}} of {{total}}",
|
|
637
|
+
"table.page": "Page {{page}}",
|
|
638
|
+
"table.firstPage": "First page",
|
|
639
|
+
"table.lastPage": "Last page",
|
|
640
|
+
"table.nextPage": "Next page",
|
|
641
|
+
"table.previousPage": "Previous page",
|
|
642
|
+
"table.sortAsc": "Sort ascending",
|
|
643
|
+
"table.sortDesc": "Sort descending",
|
|
644
|
+
"table.columns": "Columns",
|
|
645
|
+
"table.hideColumn": "Hide column",
|
|
646
|
+
"table.showColumn": "Show column",
|
|
647
|
+
"upload.dropzone": "Drop files here or click to upload",
|
|
648
|
+
"upload.browse": "Browse files",
|
|
649
|
+
"upload.uploading": "Uploading...",
|
|
650
|
+
"upload.complete": "Upload complete",
|
|
651
|
+
"upload.error": "Upload failed",
|
|
652
|
+
"upload.maxSize": "File must be smaller than {{size}}",
|
|
653
|
+
"upload.invalidType": "Invalid file type. Allowed: {{types}}",
|
|
654
|
+
"upload.remove": "Remove file",
|
|
655
|
+
"upload.replace": "Replace file",
|
|
656
|
+
"upload.preview": "Preview",
|
|
657
|
+
"upload.noFile": "No file selected",
|
|
658
|
+
"upload.dragDrop": "Drag and drop files here",
|
|
659
|
+
"editor.bold": "Bold",
|
|
660
|
+
"editor.italic": "Italic",
|
|
661
|
+
"editor.underline": "Underline",
|
|
662
|
+
"editor.strikethrough": "Strikethrough",
|
|
663
|
+
"editor.heading": "Heading {{level}}",
|
|
664
|
+
"editor.link": "Insert link",
|
|
665
|
+
"editor.image": "Insert image",
|
|
666
|
+
"editor.list": "List",
|
|
667
|
+
"editor.orderedList": "Numbered list",
|
|
668
|
+
"editor.unorderedList": "Bullet list",
|
|
669
|
+
"editor.quote": "Quote",
|
|
670
|
+
"editor.code": "Code",
|
|
671
|
+
"editor.codeBlock": "Code block",
|
|
672
|
+
"editor.table": "Insert table",
|
|
673
|
+
"editor.undo": "Undo",
|
|
674
|
+
"editor.redo": "Redo",
|
|
675
|
+
"editor.alignLeft": "Align left",
|
|
676
|
+
"editor.alignCenter": "Align center",
|
|
677
|
+
"editor.alignRight": "Align right",
|
|
678
|
+
"editor.alignJustify": "Justify",
|
|
679
|
+
"editor.horizontalRule": "Horizontal rule",
|
|
680
|
+
"editor.addRowBefore": "Add row before",
|
|
681
|
+
"editor.addRowAfter": "Add row after",
|
|
682
|
+
"editor.addColumnBefore": "Add column before",
|
|
683
|
+
"editor.addColumnAfter": "Add column after",
|
|
684
|
+
"editor.deleteRow": "Delete row",
|
|
685
|
+
"editor.deleteColumn": "Delete column",
|
|
686
|
+
"editor.deleteTable": "Delete table",
|
|
687
|
+
"editor.toggleHeaderRow": "Toggle header row",
|
|
688
|
+
"editor.toggleHeaderColumn": "Toggle header column",
|
|
689
|
+
"editor.mergeCells": "Merge cells",
|
|
690
|
+
"editor.splitCell": "Split cell",
|
|
691
|
+
"editor.insertUrl": "Insert URL",
|
|
692
|
+
"editor.altText": "Alt text (optional)",
|
|
693
|
+
"editor.uploadFile": "Upload file",
|
|
694
|
+
"editor.chooseFile": "Choose file",
|
|
695
|
+
"editor.uploading": "Uploading...",
|
|
696
|
+
"editor.browseLibrary": "Browse library",
|
|
697
|
+
"toast.success": "Success",
|
|
698
|
+
"toast.error": "Error",
|
|
699
|
+
"toast.warning": "Warning",
|
|
700
|
+
"toast.info": "Info",
|
|
701
|
+
"toast.saving": "Saving...",
|
|
702
|
+
"toast.saveFailed": "Failed to save changes",
|
|
703
|
+
"toast.saveSuccess": "Changes saved successfully",
|
|
704
|
+
"toast.creating": "Creating...",
|
|
705
|
+
"toast.createSuccess": "Created successfully",
|
|
706
|
+
"toast.createFailed": "Failed to create",
|
|
707
|
+
"toast.deleting": "Deleting...",
|
|
708
|
+
"toast.deleteFailed": "Failed to delete",
|
|
709
|
+
"toast.deleteSuccess": "Deleted successfully",
|
|
710
|
+
"toast.loadFailed": "Failed to load data",
|
|
711
|
+
"toast.uploadFailed": "Failed to upload file",
|
|
712
|
+
"toast.uploadSuccess": "File uploaded successfully",
|
|
713
|
+
"toast.copySuccess": "Copied to clipboard",
|
|
714
|
+
"toast.copyFailed": "Failed to copy to clipboard",
|
|
715
|
+
"toast.idCopied": "ID copied to clipboard",
|
|
716
|
+
"toast.validationFailed": "Validation failed",
|
|
717
|
+
"toast.validationDescription": "Please check the form for errors",
|
|
718
|
+
"toast.created": "{{name}} created",
|
|
719
|
+
"toast.updated": "{{name}} updated",
|
|
720
|
+
"toast.resourceSaveFailed": "Failed to save {{name}}",
|
|
721
|
+
"toast.editComingSoon": "Edit functionality coming soon",
|
|
722
|
+
"toast.maxFilesWarning": "Only {{remaining}} more file(s) can be added (max {{max}})",
|
|
723
|
+
"toast.settingsSaveFailed": "Failed to save settings",
|
|
724
|
+
"toast.actionSuccess": "Action completed successfully",
|
|
725
|
+
"toast.actionFailed": "Action failed",
|
|
726
|
+
"toast.localeChangedUnsaved": "Content language changed",
|
|
727
|
+
"toast.localeChangedUnsavedDescription": "Your unsaved changes were replaced with content from the new language.",
|
|
728
|
+
"confirm.delete": "Are you sure you want to delete this? This action cannot be undone.",
|
|
729
|
+
"confirm.discard": "Are you sure you want to discard your changes? This action cannot be undone.",
|
|
730
|
+
"confirm.unsavedChanges": "You have unsaved changes. Are you sure you want to leave?",
|
|
731
|
+
"confirm.action": "Are you sure you want to continue?",
|
|
732
|
+
"confirm.irreversible": "This action cannot be undone.",
|
|
733
|
+
"confirm.localeChange": "Discard unsaved changes?",
|
|
734
|
+
"confirm.localeChangeDescription": "You have unsaved changes. Switching content language will discard your changes and load the content in the new language.",
|
|
735
|
+
"confirm.localeChangeStay": "Stay",
|
|
736
|
+
"confirm.localeChangeDiscard": "Discard & switch",
|
|
737
|
+
"status.draft": "Draft",
|
|
738
|
+
"status.published": "Published",
|
|
739
|
+
"status.archived": "Archived",
|
|
740
|
+
"status.pending": "Pending",
|
|
741
|
+
"status.active": "Active",
|
|
742
|
+
"status.inactive": "Inactive",
|
|
743
|
+
"date.today": "Today",
|
|
744
|
+
"date.yesterday": "Yesterday",
|
|
745
|
+
"date.tomorrow": "Tomorrow",
|
|
746
|
+
"date.selectDate": "Select date",
|
|
747
|
+
"date.selectTime": "Select time",
|
|
748
|
+
"date.clear": "Clear date",
|
|
749
|
+
"a11y.openMenu": "Open menu",
|
|
750
|
+
"a11y.closeMenu": "Close menu",
|
|
751
|
+
"a11y.expand": "Expand",
|
|
752
|
+
"a11y.collapse": "Collapse",
|
|
753
|
+
"a11y.loading": "Loading",
|
|
754
|
+
"a11y.required": "Required",
|
|
755
|
+
"a11y.optional": "Optional",
|
|
756
|
+
"a11y.selected": "Selected",
|
|
757
|
+
"a11y.notSelected": "Not selected",
|
|
758
|
+
"locale.language": "Language",
|
|
759
|
+
"locale.switchLanguage": "Switch language",
|
|
760
|
+
"locale.contentLanguage": "Content language",
|
|
761
|
+
"locale.uiLanguage": "Interface language",
|
|
762
|
+
"defaults.users.label": "Users",
|
|
763
|
+
"defaults.users.description": "Manage admin users and their roles",
|
|
764
|
+
"defaults.users.fields.name.label": "Name",
|
|
765
|
+
"defaults.users.fields.name.placeholder": "Enter user name",
|
|
766
|
+
"defaults.users.fields.email.label": "Email",
|
|
767
|
+
"defaults.users.fields.email.description": "Email address (read-only)",
|
|
768
|
+
"defaults.users.fields.role.label": "Role",
|
|
769
|
+
"defaults.users.fields.role.options.admin": "Admin",
|
|
770
|
+
"defaults.users.fields.role.options.user": "User",
|
|
771
|
+
"defaults.users.fields.emailVerified.label": "Email Verified",
|
|
772
|
+
"defaults.users.fields.emailVerified.description": "Whether the user has verified their email address",
|
|
773
|
+
"defaults.users.fields.banned.label": "Banned",
|
|
774
|
+
"defaults.users.fields.banned.description": "Prevent user from accessing the system",
|
|
775
|
+
"defaults.users.fields.banReason.label": "Ban Reason",
|
|
776
|
+
"defaults.users.fields.banReason.placeholder": "Enter reason for banning...",
|
|
777
|
+
"defaults.users.sections.basicInfo": "Basic Information",
|
|
778
|
+
"defaults.users.sections.permissions": "Permissions",
|
|
779
|
+
"defaults.users.sections.accessControl": "Access Control",
|
|
780
|
+
"defaults.users.actions.createUser.label": "Create User",
|
|
781
|
+
"defaults.users.actions.createUser.title": "Create User",
|
|
782
|
+
"defaults.users.actions.createUser.description": "Create a new user account with login credentials.",
|
|
783
|
+
"defaults.users.actions.createUser.fields.password.label": "Password",
|
|
784
|
+
"defaults.users.actions.createUser.fields.password.placeholder": "Enter password",
|
|
785
|
+
"defaults.users.actions.createUser.submit": "Create User",
|
|
786
|
+
"defaults.users.actions.createUser.success": "User {{email}} created successfully. Share the credentials with the user.",
|
|
787
|
+
"defaults.users.actions.createUser.errorNoAuth": "Auth client not configured. Cannot create user.",
|
|
788
|
+
"defaults.users.actions.resetPassword.label": "Reset Password",
|
|
789
|
+
"defaults.users.actions.resetPassword.title": "Reset Password",
|
|
790
|
+
"defaults.users.actions.resetPassword.description": "Set a new password for this user.",
|
|
791
|
+
"defaults.users.actions.resetPassword.fields.newPassword.label": "New Password",
|
|
792
|
+
"defaults.users.actions.resetPassword.fields.newPassword.placeholder": "Enter new password",
|
|
793
|
+
"defaults.users.actions.resetPassword.fields.confirmPassword.label": "Confirm Password",
|
|
794
|
+
"defaults.users.actions.resetPassword.fields.confirmPassword.placeholder": "Confirm new password",
|
|
795
|
+
"defaults.users.actions.resetPassword.submit": "Reset Password",
|
|
796
|
+
"defaults.users.actions.resetPassword.success": "Password reset successfully!",
|
|
797
|
+
"defaults.users.actions.resetPassword.errorMismatch": "Passwords do not match",
|
|
798
|
+
"defaults.users.actions.delete.label": "Delete User",
|
|
799
|
+
"defaults.assets.label": "Media",
|
|
800
|
+
"defaults.assets.description": "Manage uploaded files and images",
|
|
801
|
+
"defaults.assets.fields.preview.label": "Preview",
|
|
802
|
+
"defaults.assets.fields.filename.label": "Filename",
|
|
803
|
+
"defaults.assets.fields.filename.description": "Original filename of the uploaded file",
|
|
804
|
+
"defaults.assets.fields.mimeType.label": "Type",
|
|
805
|
+
"defaults.assets.fields.mimeType.description": "MIME type of the file",
|
|
806
|
+
"defaults.assets.fields.size.label": "Size (bytes)",
|
|
807
|
+
"defaults.assets.fields.size.description": "File size in bytes",
|
|
808
|
+
"defaults.assets.fields.alt.label": "Alt Text",
|
|
809
|
+
"defaults.assets.fields.alt.placeholder": "Describe the image for accessibility",
|
|
810
|
+
"defaults.assets.fields.alt.description": "Alternative text for screen readers",
|
|
811
|
+
"defaults.assets.fields.caption.label": "Caption",
|
|
812
|
+
"defaults.assets.fields.caption.placeholder": "Add a caption...",
|
|
813
|
+
"defaults.assets.fields.visibility.label": "Visibility",
|
|
814
|
+
"defaults.assets.fields.visibility.options.public": "Public",
|
|
815
|
+
"defaults.assets.fields.visibility.options.private": "Private",
|
|
816
|
+
"defaults.assets.fields.visibility.description": "Public files are accessible without authentication. Private files require a signed URL.",
|
|
817
|
+
"defaults.assets.sections.fileInfo": "File Information",
|
|
818
|
+
"defaults.assets.sections.metadata": "Metadata",
|
|
819
|
+
"defaults.assets.sections.metadata.description": "Add descriptive information for accessibility and SEO",
|
|
820
|
+
"defaults.assets.actions.upload.label": "Upload Files",
|
|
821
|
+
"defaults.sidebar.administration": "Administration",
|
|
822
|
+
"viewOptions.title": "View Options",
|
|
823
|
+
"viewOptions.columns": "Columns",
|
|
824
|
+
"viewOptions.filters": "Filters",
|
|
825
|
+
"viewOptions.savedViews": "Saved Views",
|
|
826
|
+
"viewOptions.apply": "Apply",
|
|
827
|
+
"viewOptions.reset": "Reset",
|
|
828
|
+
"viewOptions.saveCurrentConfig": "Save Current Configuration",
|
|
829
|
+
"viewOptions.viewNamePlaceholder": "View Name...",
|
|
830
|
+
"viewOptions.saveDescription": "Saves current columns, filters, and sort order.",
|
|
831
|
+
"viewOptions.noChangesToSave": "No filters or column changes to save.",
|
|
832
|
+
"viewOptions.noSavedViews": "No saved views yet.",
|
|
833
|
+
"viewOptions.filtersCount": {
|
|
834
|
+
one: "{{count}} filter",
|
|
835
|
+
other: "{{count}} filters"
|
|
836
|
+
},
|
|
837
|
+
"viewOptions.columnsCount": {
|
|
838
|
+
one: "{{count}} col",
|
|
839
|
+
other: "{{count}} cols"
|
|
840
|
+
},
|
|
841
|
+
"viewOptions.defaultView": "Default",
|
|
842
|
+
"viewOptions.columnsDragHint": "Drag to reorder, toggle to show/hide columns.",
|
|
843
|
+
"viewOptions.noFieldsAvailable": "No fields available.",
|
|
844
|
+
"viewOptions.filtersDescription": "Narrow down your results with custom rules.",
|
|
845
|
+
"viewOptions.filterNumber": "Filter #{{number}}",
|
|
846
|
+
"viewOptions.selectField": "Select field",
|
|
847
|
+
"viewOptions.selectOperator": "Select operator",
|
|
848
|
+
"viewOptions.valuePlaceholder": "Value...",
|
|
849
|
+
"viewOptions.noActiveFilters": "No active filters.",
|
|
850
|
+
"viewOptions.addFilter": "Add Filter",
|
|
851
|
+
"viewOptions.clearAll": "Clear All",
|
|
852
|
+
"viewOptions.activeFilters": {
|
|
853
|
+
one: "{{count}} filter active",
|
|
854
|
+
other: "{{count}} filters active"
|
|
855
|
+
},
|
|
856
|
+
"viewOptions.clearFilters": "Clear filters",
|
|
857
|
+
"filter.contains": "Contains",
|
|
858
|
+
"filter.notContains": "Does not contain",
|
|
859
|
+
"filter.equals": "Equals",
|
|
860
|
+
"filter.notEquals": "Does not equal",
|
|
861
|
+
"filter.startsWith": "Starts with",
|
|
862
|
+
"filter.endsWith": "Ends with",
|
|
863
|
+
"filter.greaterThan": "Greater than",
|
|
864
|
+
"filter.greaterThanOrEqual": "Greater than or equal",
|
|
865
|
+
"filter.lessThan": "Less than",
|
|
866
|
+
"filter.lessThanOrEqual": "Less than or equal",
|
|
867
|
+
"filter.in": "Is any of",
|
|
868
|
+
"filter.notIn": "Is none of",
|
|
869
|
+
"filter.some": "Has any",
|
|
870
|
+
"filter.every": "Has all",
|
|
871
|
+
"filter.none": "Has none",
|
|
872
|
+
"filter.isEmpty": "Is empty",
|
|
873
|
+
"filter.isNotEmpty": "Is not empty",
|
|
874
|
+
"preview.show": "Preview",
|
|
875
|
+
"preview.hide": "Hide Preview",
|
|
876
|
+
"preview.title": "Preview",
|
|
877
|
+
"preview.livePreview": "Live Preview",
|
|
878
|
+
"preview.fullscreen": "Fullscreen",
|
|
879
|
+
"preview.close": "Close preview",
|
|
880
|
+
"preview.loading": "Loading preview...",
|
|
881
|
+
"autosave.saving": "Saving...",
|
|
882
|
+
"autosave.saved": "Saved",
|
|
883
|
+
"autosave.unsavedChanges": "Unsaved changes",
|
|
884
|
+
"autosave.justNow": "just now",
|
|
885
|
+
"autosave.secondsAgo": {
|
|
886
|
+
one: "{{count}}s ago",
|
|
887
|
+
other: "{{count}}s ago"
|
|
888
|
+
},
|
|
889
|
+
"autosave.minutesAgo": {
|
|
890
|
+
one: "{{count}}m ago",
|
|
891
|
+
other: "{{count}}m ago"
|
|
892
|
+
},
|
|
893
|
+
"autosave.hoursAgo": {
|
|
894
|
+
one: "{{count}}h ago",
|
|
895
|
+
other: "{{count}}h ago"
|
|
896
|
+
},
|
|
897
|
+
"globalSearch.placeholder": "Search collections, globals, actions, records...",
|
|
898
|
+
"globalSearch.collections": "Collections",
|
|
899
|
+
"globalSearch.globals": "Globals",
|
|
900
|
+
"globalSearch.quickActions": "Quick Actions",
|
|
901
|
+
"globalSearch.records": "Records",
|
|
902
|
+
"globalSearch.createNew": "Create new {{name}}",
|
|
903
|
+
"globalSearch.noResults": "No results found",
|
|
904
|
+
"globalSearch.searching": "Searching...",
|
|
905
|
+
"globalSearch.navigate": "to navigate",
|
|
906
|
+
"globalSearch.select": "to select",
|
|
907
|
+
"collectionSearch.placeholder": "Search records...",
|
|
908
|
+
"collectionSearch.noResults": "No matching records found",
|
|
909
|
+
"collectionSearch.searching": "Searching..."
|
|
910
|
+
};
|
|
911
|
+
|
|
912
|
+
//#endregion
|
|
913
|
+
//#region src/client/i18n/messages.ts
|
|
914
|
+
/**
|
|
915
|
+
* Default Admin UI Messages
|
|
916
|
+
*
|
|
917
|
+
* Re-exports English messages as the default language.
|
|
918
|
+
* For other languages, import from "@questpie/admin/i18n/messages".
|
|
919
|
+
*
|
|
920
|
+
* @example
|
|
921
|
+
* ```ts
|
|
922
|
+
* // Default English (always bundled)
|
|
923
|
+
* import { adminMessages } from "@questpie/admin";
|
|
924
|
+
*
|
|
925
|
+
* // Add other languages
|
|
926
|
+
* import { adminMessagesSK } from "@questpie/admin/i18n/messages";
|
|
927
|
+
*
|
|
928
|
+
* const admin = qa()
|
|
929
|
+
* .translations({ sk: adminMessagesSK })
|
|
930
|
+
* .build();
|
|
931
|
+
* ```
|
|
932
|
+
*/
|
|
933
|
+
/**
|
|
934
|
+
* Default English messages for admin UI
|
|
935
|
+
* This is re-exported from messages/en.ts for backwards compatibility
|
|
936
|
+
*/
|
|
937
|
+
const adminMessages = adminMessagesEN;
|
|
938
|
+
/**
|
|
939
|
+
* Convert validation messages to SimpleMessages format
|
|
940
|
+
*/
|
|
941
|
+
function flattenValidationMessages(messages) {
|
|
942
|
+
const result = {};
|
|
943
|
+
for (const [key, value] of Object.entries(messages)) if (typeof value === "string") result[key] = value;
|
|
944
|
+
else result[key] = {
|
|
945
|
+
one: value.one,
|
|
946
|
+
other: value.other
|
|
947
|
+
};
|
|
948
|
+
return result;
|
|
949
|
+
}
|
|
950
|
+
/**
|
|
951
|
+
* All admin messages including shared validation messages
|
|
952
|
+
*/
|
|
953
|
+
const allAdminMessages = {
|
|
954
|
+
...flattenValidationMessages(validationMessagesEN),
|
|
955
|
+
...adminMessages
|
|
956
|
+
};
|
|
957
|
+
|
|
958
|
+
//#endregion
|
|
959
|
+
//#region src/client/i18n/intl-cache.ts
|
|
960
|
+
/**
|
|
961
|
+
* Intl Formatters Cache
|
|
962
|
+
*
|
|
963
|
+
* Caches Intl formatter instances to avoid creating new ones on every call.
|
|
964
|
+
* Intl formatters are expensive to create but cheap to reuse.
|
|
965
|
+
*/
|
|
966
|
+
function getCacheKey(locale, options) {
|
|
967
|
+
if (!options || Object.keys(options).length === 0) return locale;
|
|
968
|
+
return `${locale}:${JSON.stringify(options)}`;
|
|
969
|
+
}
|
|
970
|
+
const dateTimeFormatCache = /* @__PURE__ */ new Map();
|
|
971
|
+
function getDateTimeFormat(locale, options) {
|
|
972
|
+
const key = getCacheKey(locale, options);
|
|
973
|
+
let formatter = dateTimeFormatCache.get(key);
|
|
974
|
+
if (!formatter) {
|
|
975
|
+
formatter = new Intl.DateTimeFormat(locale, options);
|
|
976
|
+
dateTimeFormatCache.set(key, formatter);
|
|
977
|
+
}
|
|
978
|
+
return formatter;
|
|
979
|
+
}
|
|
980
|
+
const numberFormatCache = /* @__PURE__ */ new Map();
|
|
981
|
+
function getNumberFormat(locale, options) {
|
|
982
|
+
const key = getCacheKey(locale, options);
|
|
983
|
+
let formatter = numberFormatCache.get(key);
|
|
984
|
+
if (!formatter) {
|
|
985
|
+
formatter = new Intl.NumberFormat(locale, options);
|
|
986
|
+
numberFormatCache.set(key, formatter);
|
|
987
|
+
}
|
|
988
|
+
return formatter;
|
|
989
|
+
}
|
|
990
|
+
const pluralRulesCache = /* @__PURE__ */ new Map();
|
|
991
|
+
function getPluralRules(locale, options) {
|
|
992
|
+
const key = getCacheKey(locale, options);
|
|
993
|
+
let rules = pluralRulesCache.get(key);
|
|
994
|
+
if (!rules) {
|
|
995
|
+
rules = new Intl.PluralRules(locale, options);
|
|
996
|
+
pluralRulesCache.set(key, rules);
|
|
997
|
+
}
|
|
998
|
+
return rules;
|
|
999
|
+
}
|
|
1000
|
+
const relativeTimeFormatCache = /* @__PURE__ */ new Map();
|
|
1001
|
+
function getRelativeTimeFormat(locale, options) {
|
|
1002
|
+
const key = getCacheKey(locale, options);
|
|
1003
|
+
let formatter = relativeTimeFormatCache.get(key);
|
|
1004
|
+
if (!formatter) {
|
|
1005
|
+
formatter = new Intl.RelativeTimeFormat(locale, options);
|
|
1006
|
+
relativeTimeFormatCache.set(key, formatter);
|
|
1007
|
+
}
|
|
1008
|
+
return formatter;
|
|
1009
|
+
}
|
|
1010
|
+
const displayNamesCache = /* @__PURE__ */ new Map();
|
|
1011
|
+
function getDisplayNames(locale, options) {
|
|
1012
|
+
const key = getCacheKey(locale, options);
|
|
1013
|
+
let displayNames = displayNamesCache.get(key);
|
|
1014
|
+
if (!displayNames) {
|
|
1015
|
+
displayNames = new Intl.DisplayNames([locale], options);
|
|
1016
|
+
displayNamesCache.set(key, displayNames);
|
|
1017
|
+
}
|
|
1018
|
+
return displayNames;
|
|
1019
|
+
}
|
|
1020
|
+
|
|
1021
|
+
//#endregion
|
|
1022
|
+
//#region src/client/i18n/simple.ts
|
|
1023
|
+
/**
|
|
1024
|
+
* Simple I18n Implementation
|
|
1025
|
+
*
|
|
1026
|
+
* A minimal i18n implementation for basic use cases.
|
|
1027
|
+
* For full i18n features (pluralization, ICU, etc.), use i18next or react-intl.
|
|
1028
|
+
*
|
|
1029
|
+
* Features:
|
|
1030
|
+
* - Basic key-value translation
|
|
1031
|
+
* - Simple interpolation ({{key}})
|
|
1032
|
+
* - Uses Intl APIs for formatting
|
|
1033
|
+
* - Basic pluralization via Intl.PluralRules
|
|
1034
|
+
*/
|
|
1035
|
+
const RTL_LOCALES = new Set([
|
|
1036
|
+
"ar",
|
|
1037
|
+
"he",
|
|
1038
|
+
"fa",
|
|
1039
|
+
"ur",
|
|
1040
|
+
"ps",
|
|
1041
|
+
"sd",
|
|
1042
|
+
"yi"
|
|
1043
|
+
]);
|
|
1044
|
+
/**
|
|
1045
|
+
* Create a simple i18n adapter
|
|
1046
|
+
*
|
|
1047
|
+
* @example
|
|
1048
|
+
* ```ts
|
|
1049
|
+
* const i18n = createSimpleI18n({
|
|
1050
|
+
* locale: "en",
|
|
1051
|
+
* locales: ["en", "de"],
|
|
1052
|
+
* messages: {
|
|
1053
|
+
* en: {
|
|
1054
|
+
* "common.save": "Save",
|
|
1055
|
+
* "items.count": { one: "{{count}} item", other: "{{count}} items" }
|
|
1056
|
+
* },
|
|
1057
|
+
* de: {
|
|
1058
|
+
* "common.save": "Speichern",
|
|
1059
|
+
* "items.count": { one: "{{count}} Artikel", other: "{{count}} Artikel" }
|
|
1060
|
+
* }
|
|
1061
|
+
* }
|
|
1062
|
+
* });
|
|
1063
|
+
* ```
|
|
1064
|
+
*/
|
|
1065
|
+
function createSimpleI18n(options) {
|
|
1066
|
+
let currentLocale = options.locale;
|
|
1067
|
+
const { locales, messages, fallbackLocale = locales[0], onLocaleChange: onLocaleChangeCallback } = options;
|
|
1068
|
+
const listeners = /* @__PURE__ */ new Set();
|
|
1069
|
+
/**
|
|
1070
|
+
* Notify all listeners about locale change
|
|
1071
|
+
*/
|
|
1072
|
+
function notifyListeners(locale) {
|
|
1073
|
+
onLocaleChangeCallback?.(locale);
|
|
1074
|
+
for (const listener of listeners) listener(locale);
|
|
1075
|
+
}
|
|
1076
|
+
/**
|
|
1077
|
+
* Get message for key in locale
|
|
1078
|
+
*/
|
|
1079
|
+
function getMessage(key, locale) {
|
|
1080
|
+
return messages[locale]?.[key];
|
|
1081
|
+
}
|
|
1082
|
+
/**
|
|
1083
|
+
* Interpolate values into string
|
|
1084
|
+
*/
|
|
1085
|
+
function interpolate(str, params) {
|
|
1086
|
+
if (!params) return str;
|
|
1087
|
+
return str.replace(/\{\{(\w+)\}\}/g, (_, key) => {
|
|
1088
|
+
const value = params[key];
|
|
1089
|
+
return value !== void 0 ? String(value) : `{{${key}}}`;
|
|
1090
|
+
});
|
|
1091
|
+
}
|
|
1092
|
+
/**
|
|
1093
|
+
* Check if value is plural messages
|
|
1094
|
+
*/
|
|
1095
|
+
function isPluralMessages(value) {
|
|
1096
|
+
return typeof value === "object" && value !== null && "one" in value && "other" in value;
|
|
1097
|
+
}
|
|
1098
|
+
/**
|
|
1099
|
+
* Get plural form
|
|
1100
|
+
*/
|
|
1101
|
+
function getPluralForm(forms, count, locale) {
|
|
1102
|
+
switch (getPluralRules(locale).select(count)) {
|
|
1103
|
+
case "zero": return forms.zero ?? forms.other;
|
|
1104
|
+
case "one": return forms.one;
|
|
1105
|
+
case "two": return forms.two ?? forms.other;
|
|
1106
|
+
case "few": return forms.few ?? forms.other;
|
|
1107
|
+
case "many": return forms.many ?? forms.other;
|
|
1108
|
+
default: return forms.other;
|
|
1109
|
+
}
|
|
1110
|
+
}
|
|
1111
|
+
return {
|
|
1112
|
+
get locale() {
|
|
1113
|
+
return currentLocale;
|
|
1114
|
+
},
|
|
1115
|
+
get locales() {
|
|
1116
|
+
return locales;
|
|
1117
|
+
},
|
|
1118
|
+
t(key, params) {
|
|
1119
|
+
let value = getMessage(key, currentLocale);
|
|
1120
|
+
if (value === void 0 && currentLocale !== fallbackLocale) value = getMessage(key, fallbackLocale);
|
|
1121
|
+
if (value === void 0) return key;
|
|
1122
|
+
if (isPluralMessages(value)) {
|
|
1123
|
+
const count = typeof params?.count === "number" ? params.count : 1;
|
|
1124
|
+
return interpolate(getPluralForm(value, count, currentLocale), params);
|
|
1125
|
+
}
|
|
1126
|
+
return interpolate(value, params);
|
|
1127
|
+
},
|
|
1128
|
+
setLocale(locale) {
|
|
1129
|
+
if (locales.includes(locale) && locale !== currentLocale) {
|
|
1130
|
+
currentLocale = locale;
|
|
1131
|
+
notifyListeners(locale);
|
|
1132
|
+
}
|
|
1133
|
+
},
|
|
1134
|
+
onLocaleChange(callback) {
|
|
1135
|
+
listeners.add(callback);
|
|
1136
|
+
return () => {
|
|
1137
|
+
listeners.delete(callback);
|
|
1138
|
+
};
|
|
1139
|
+
},
|
|
1140
|
+
formatDate(date, opts) {
|
|
1141
|
+
const d = typeof date === "number" ? new Date(date) : date;
|
|
1142
|
+
return getDateTimeFormat(currentLocale, opts).format(d);
|
|
1143
|
+
},
|
|
1144
|
+
formatNumber(value, opts) {
|
|
1145
|
+
return getNumberFormat(currentLocale, opts).format(value);
|
|
1146
|
+
},
|
|
1147
|
+
formatRelative(date) {
|
|
1148
|
+
const d = typeof date === "number" ? new Date(date) : date;
|
|
1149
|
+
const now = Date.now();
|
|
1150
|
+
const diff = d.getTime() - now;
|
|
1151
|
+
const diffSeconds = Math.round(diff / 1e3);
|
|
1152
|
+
const diffMinutes = Math.round(diff / 6e4);
|
|
1153
|
+
const diffHours = Math.round(diff / 36e5);
|
|
1154
|
+
const diffDays = Math.round(diff / 864e5);
|
|
1155
|
+
const rtf = getRelativeTimeFormat(currentLocale, { numeric: "auto" });
|
|
1156
|
+
if (Math.abs(diffSeconds) < 60) return rtf.format(diffSeconds, "second");
|
|
1157
|
+
if (Math.abs(diffMinutes) < 60) return rtf.format(diffMinutes, "minute");
|
|
1158
|
+
if (Math.abs(diffHours) < 24) return rtf.format(diffHours, "hour");
|
|
1159
|
+
return rtf.format(diffDays, "day");
|
|
1160
|
+
},
|
|
1161
|
+
getLocaleName(locale) {
|
|
1162
|
+
try {
|
|
1163
|
+
return getDisplayNames(currentLocale, { type: "language" }).of(locale) ?? locale;
|
|
1164
|
+
} catch {
|
|
1165
|
+
return locale;
|
|
1166
|
+
}
|
|
1167
|
+
},
|
|
1168
|
+
isRTL() {
|
|
1169
|
+
return RTL_LOCALES.has(currentLocale);
|
|
1170
|
+
}
|
|
1171
|
+
};
|
|
1172
|
+
}
|
|
1173
|
+
|
|
1174
|
+
//#endregion
|
|
1175
|
+
//#region src/client/runtime/routes.ts
|
|
1176
|
+
/**
|
|
1177
|
+
* Build type-safe routes from admin configuration
|
|
1178
|
+
*
|
|
1179
|
+
* @example
|
|
1180
|
+
* ```ts
|
|
1181
|
+
* import { buildRoutes } from "@questpie/admin/runtime";
|
|
1182
|
+
* import { appAdmin } from "./admin";
|
|
1183
|
+
*
|
|
1184
|
+
* const routes = buildRoutes(appAdmin);
|
|
1185
|
+
*
|
|
1186
|
+
* // Type-safe route generation
|
|
1187
|
+
* routes.dashboard(); // "/admin"
|
|
1188
|
+
* routes.collections.posts.list(); // "/admin/collections/posts"
|
|
1189
|
+
* routes.collections.posts.edit("123"); // "/admin/collections/posts/123/edit"
|
|
1190
|
+
* routes.globals.settings.edit(); // "/admin/globals/settings"
|
|
1191
|
+
* ```
|
|
1192
|
+
*/
|
|
1193
|
+
function buildRoutes(admin, options = {}) {
|
|
1194
|
+
const { basePath = "/admin" } = options;
|
|
1195
|
+
const path = (...segments) => [basePath, ...segments].filter(Boolean).join("/");
|
|
1196
|
+
const collectionNames = admin.getCollectionNames();
|
|
1197
|
+
const collections = {};
|
|
1198
|
+
for (const name of collectionNames) collections[name] = {
|
|
1199
|
+
list: () => path("collections", name),
|
|
1200
|
+
create: () => path("collections", name, "create"),
|
|
1201
|
+
edit: (id) => path("collections", name, id, "edit"),
|
|
1202
|
+
view: (id) => path("collections", name, id)
|
|
1203
|
+
};
|
|
1204
|
+
const globalNames = admin.getGlobalNames();
|
|
1205
|
+
const globals = {};
|
|
1206
|
+
for (const name of globalNames) globals[name] = { edit: () => path("globals", name) };
|
|
1207
|
+
const pageConfigs = admin.getPages();
|
|
1208
|
+
const pages = {};
|
|
1209
|
+
for (const [name, config] of Object.entries(pageConfigs)) {
|
|
1210
|
+
const pagePath = config.path ?? name;
|
|
1211
|
+
pages[name] = { view: () => pagePath.startsWith("/") ? pagePath : path("pages", pagePath) };
|
|
1212
|
+
}
|
|
1213
|
+
return {
|
|
1214
|
+
dashboard: () => basePath,
|
|
1215
|
+
collections,
|
|
1216
|
+
globals,
|
|
1217
|
+
pages,
|
|
1218
|
+
auth: {
|
|
1219
|
+
login: () => path("auth", "login"),
|
|
1220
|
+
logout: () => path("auth", "logout"),
|
|
1221
|
+
forgotPassword: () => path("auth", "forgot-password"),
|
|
1222
|
+
resetPassword: (token) => path("auth", "reset-password", token)
|
|
1223
|
+
}
|
|
1224
|
+
};
|
|
1225
|
+
}
|
|
1226
|
+
/**
|
|
1227
|
+
* Build navigation structure from admin configuration
|
|
1228
|
+
*
|
|
1229
|
+
* @example
|
|
1230
|
+
* ```ts
|
|
1231
|
+
* import { buildNavigation } from "@questpie/admin/runtime";
|
|
1232
|
+
* import { appAdmin } from "./admin";
|
|
1233
|
+
*
|
|
1234
|
+
* const navigation = buildNavigation(appAdmin);
|
|
1235
|
+
* // Returns grouped navigation items for sidebar rendering
|
|
1236
|
+
* ```
|
|
1237
|
+
*/
|
|
1238
|
+
function buildNavigation(admin, options = {}) {
|
|
1239
|
+
const routes = buildRoutes(admin, options);
|
|
1240
|
+
const items = [];
|
|
1241
|
+
items.push({
|
|
1242
|
+
id: "dashboard",
|
|
1243
|
+
label: "Dashboard",
|
|
1244
|
+
href: routes.dashboard(),
|
|
1245
|
+
icon: void 0,
|
|
1246
|
+
type: "dashboard",
|
|
1247
|
+
order: -1e3
|
|
1248
|
+
});
|
|
1249
|
+
for (const [name, config] of Object.entries(admin.getCollections())) {
|
|
1250
|
+
const meta = config.meta ?? config;
|
|
1251
|
+
if (meta.hidden) continue;
|
|
1252
|
+
const collectionRoutes = routes.collections[name];
|
|
1253
|
+
if (!collectionRoutes) continue;
|
|
1254
|
+
items.push({
|
|
1255
|
+
id: `collection:${name}`,
|
|
1256
|
+
label: resolveLabel(meta.label, name),
|
|
1257
|
+
href: collectionRoutes.list(),
|
|
1258
|
+
icon: resolveIcon(meta.icon),
|
|
1259
|
+
group: meta.group,
|
|
1260
|
+
order: meta.order ?? 0,
|
|
1261
|
+
type: "collection"
|
|
1262
|
+
});
|
|
1263
|
+
}
|
|
1264
|
+
for (const [name, config] of Object.entries(admin.getGlobals())) {
|
|
1265
|
+
const meta = config.meta ?? config;
|
|
1266
|
+
if (meta.hidden) continue;
|
|
1267
|
+
const globalRoutes = routes.globals[name];
|
|
1268
|
+
if (!globalRoutes) continue;
|
|
1269
|
+
items.push({
|
|
1270
|
+
id: `global:${name}`,
|
|
1271
|
+
label: resolveLabel(meta.label, name),
|
|
1272
|
+
href: globalRoutes.edit(),
|
|
1273
|
+
icon: resolveIcon(meta.icon),
|
|
1274
|
+
group: meta.group,
|
|
1275
|
+
order: meta.order ?? 0,
|
|
1276
|
+
type: "global"
|
|
1277
|
+
});
|
|
1278
|
+
}
|
|
1279
|
+
for (const [name, config] of Object.entries(admin.getPages())) {
|
|
1280
|
+
if (config.showInNav === false) continue;
|
|
1281
|
+
items.push({
|
|
1282
|
+
id: `page:${name}`,
|
|
1283
|
+
label: resolveLabel(config.label, name),
|
|
1284
|
+
href: routes.pages[name].view(),
|
|
1285
|
+
icon: resolveIcon(config.icon),
|
|
1286
|
+
group: config.group,
|
|
1287
|
+
order: config.order ?? 0,
|
|
1288
|
+
type: "page"
|
|
1289
|
+
});
|
|
1290
|
+
}
|
|
1291
|
+
return groupNavigationItems(items, admin);
|
|
1292
|
+
}
|
|
1293
|
+
/**
|
|
1294
|
+
* Resolve label - passes through I18nText for runtime resolution
|
|
1295
|
+
*/
|
|
1296
|
+
function resolveLabel(label, fallback) {
|
|
1297
|
+
if (typeof label === "string") return label;
|
|
1298
|
+
if (typeof label === "object" && label !== null) return label;
|
|
1299
|
+
return fallback.replace(/([A-Z])/g, " $1").replace(/^./, (s) => s.toUpperCase()).trim();
|
|
1300
|
+
}
|
|
1301
|
+
/**
|
|
1302
|
+
* Resolve icon to IconComponent
|
|
1303
|
+
* Only accepts React components, not strings
|
|
1304
|
+
*/
|
|
1305
|
+
function resolveIcon(icon) {
|
|
1306
|
+
if (typeof icon === "function" || typeof icon === "object" && icon !== null) return icon;
|
|
1307
|
+
}
|
|
1308
|
+
/**
|
|
1309
|
+
* Group navigation items based on sidebar configuration
|
|
1310
|
+
*/
|
|
1311
|
+
function groupNavigationItems(items, admin) {
|
|
1312
|
+
const sidebarConfig = admin.getSidebar();
|
|
1313
|
+
if (sidebarConfig.sections?.length) return sidebarConfig.sections.map((section) => ({
|
|
1314
|
+
id: section.id,
|
|
1315
|
+
label: resolveLabel(section.title, ""),
|
|
1316
|
+
icon: resolveIcon(section.icon),
|
|
1317
|
+
collapsed: section.collapsed,
|
|
1318
|
+
items: section.items.map((item) => {
|
|
1319
|
+
switch (item.type) {
|
|
1320
|
+
case "collection": {
|
|
1321
|
+
const found = items.find((i) => i.id === `collection:${item.collection}`);
|
|
1322
|
+
if (!found) return void 0;
|
|
1323
|
+
return {
|
|
1324
|
+
...found,
|
|
1325
|
+
label: item.label ?? found.label,
|
|
1326
|
+
icon: resolveIcon(item.icon) ?? found.icon
|
|
1327
|
+
};
|
|
1328
|
+
}
|
|
1329
|
+
case "global": {
|
|
1330
|
+
const found = items.find((i) => i.id === `global:${item.global}`);
|
|
1331
|
+
if (!found) return void 0;
|
|
1332
|
+
return {
|
|
1333
|
+
...found,
|
|
1334
|
+
label: item.label ?? found.label,
|
|
1335
|
+
icon: resolveIcon(item.icon) ?? found.icon
|
|
1336
|
+
};
|
|
1337
|
+
}
|
|
1338
|
+
case "page": {
|
|
1339
|
+
const found = items.find((i) => i.id === `page:${item.pageId}`);
|
|
1340
|
+
if (!found) return void 0;
|
|
1341
|
+
return {
|
|
1342
|
+
...found,
|
|
1343
|
+
label: item.label ?? found.label,
|
|
1344
|
+
icon: resolveIcon(item.icon) ?? found.icon
|
|
1345
|
+
};
|
|
1346
|
+
}
|
|
1347
|
+
case "link": return {
|
|
1348
|
+
id: `link:${item.href}`,
|
|
1349
|
+
label: resolveLabel(item.label, ""),
|
|
1350
|
+
href: item.href,
|
|
1351
|
+
icon: resolveIcon(item.icon),
|
|
1352
|
+
type: "link",
|
|
1353
|
+
order: 0
|
|
1354
|
+
};
|
|
1355
|
+
case "divider": return { type: "divider" };
|
|
1356
|
+
default: return;
|
|
1357
|
+
}
|
|
1358
|
+
}).filter((i) => i !== void 0)
|
|
1359
|
+
}));
|
|
1360
|
+
if (sidebarConfig.groups?.length) return sidebarConfig.groups.map((group) => ({
|
|
1361
|
+
label: resolveLabel(group.label, ""),
|
|
1362
|
+
items: group.items.map((item) => {
|
|
1363
|
+
if (typeof item === "string") return items.find((i) => i.id === `collection:${item}` || i.id === `global:${item}` || i.id === `page:${item}` || i.id === item);
|
|
1364
|
+
return {
|
|
1365
|
+
id: item.id,
|
|
1366
|
+
label: resolveLabel(item.label, item.id),
|
|
1367
|
+
href: item.href ?? "#",
|
|
1368
|
+
icon: resolveIcon(item.icon),
|
|
1369
|
+
type: "link",
|
|
1370
|
+
order: item.order ?? 0
|
|
1371
|
+
};
|
|
1372
|
+
}).filter((i) => i !== void 0)
|
|
1373
|
+
}));
|
|
1374
|
+
const groups = /* @__PURE__ */ new Map();
|
|
1375
|
+
const sortedItems = [...items].sort((a, b) => (a.order ?? 0) - (b.order ?? 0));
|
|
1376
|
+
for (const item of sortedItems) {
|
|
1377
|
+
const groupKey = item.group;
|
|
1378
|
+
if (!groups.has(groupKey)) groups.set(groupKey, []);
|
|
1379
|
+
groups.get(groupKey).push(item);
|
|
1380
|
+
}
|
|
1381
|
+
const result = [];
|
|
1382
|
+
const ungrouped = groups.get(void 0);
|
|
1383
|
+
if (ungrouped?.length) result.push({ items: ungrouped });
|
|
1384
|
+
for (const [label, groupItems] of groups) {
|
|
1385
|
+
if (label === void 0) continue;
|
|
1386
|
+
result.push({
|
|
1387
|
+
label,
|
|
1388
|
+
items: groupItems
|
|
1389
|
+
});
|
|
1390
|
+
}
|
|
1391
|
+
return result;
|
|
1392
|
+
}
|
|
1393
|
+
|
|
1394
|
+
//#endregion
|
|
1395
|
+
//#region src/client/runtime/provider.tsx
|
|
1396
|
+
/** Cookie for UI locale (admin interface language) */
|
|
1397
|
+
const UI_LOCALE_COOKIE = "questpie_ui_locale";
|
|
1398
|
+
/** Cookie for content locale (CMS content language) */
|
|
1399
|
+
const CONTENT_LOCALE_COOKIE = "questpie_content_locale";
|
|
1400
|
+
/** Cookie max age (1 year) */
|
|
1401
|
+
const LOCALE_COOKIE_MAX_AGE = 3600 * 24 * 365;
|
|
1402
|
+
const LEGACY_LOCALE_COOKIE = "questpie_locale";
|
|
1403
|
+
function getCookie(name) {
|
|
1404
|
+
if (typeof document === "undefined") return null;
|
|
1405
|
+
const match = document.cookie.match(/* @__PURE__ */ new RegExp(`${name}=([^;]+)`));
|
|
1406
|
+
return match ? match[1] : null;
|
|
1407
|
+
}
|
|
1408
|
+
function setCookie(name, value) {
|
|
1409
|
+
if (typeof document === "undefined") return;
|
|
1410
|
+
document.cookie = `${name}=${value}; path=/; max-age=${LOCALE_COOKIE_MAX_AGE}; SameSite=Lax`;
|
|
1411
|
+
}
|
|
1412
|
+
function getUiLocaleFromCookie() {
|
|
1413
|
+
return getCookie(UI_LOCALE_COOKIE) ?? getCookie(LEGACY_LOCALE_COOKIE);
|
|
1414
|
+
}
|
|
1415
|
+
function getContentLocaleFromCookie() {
|
|
1416
|
+
return getCookie(CONTENT_LOCALE_COOKIE) ?? getCookie(LEGACY_LOCALE_COOKIE);
|
|
1417
|
+
}
|
|
1418
|
+
function setUiLocaleCookie(locale) {
|
|
1419
|
+
setCookie(UI_LOCALE_COOKIE, locale);
|
|
1420
|
+
}
|
|
1421
|
+
function setContentLocaleCookie(locale) {
|
|
1422
|
+
setCookie(CONTENT_LOCALE_COOKIE, locale);
|
|
1423
|
+
}
|
|
1424
|
+
function createAdminStore({ admin, client, authClient, basePath, navigate, initialContentLocale }) {
|
|
1425
|
+
if (client && initialContentLocale && "setLocale" in client) client.setLocale(initialContentLocale);
|
|
1426
|
+
return createStore((set) => ({
|
|
1427
|
+
admin,
|
|
1428
|
+
client,
|
|
1429
|
+
authClient,
|
|
1430
|
+
basePath,
|
|
1431
|
+
navigate,
|
|
1432
|
+
contentLocale: initialContentLocale,
|
|
1433
|
+
setContentLocale: (newLocale) => {
|
|
1434
|
+
setContentLocaleCookie(newLocale);
|
|
1435
|
+
set({ contentLocale: newLocale });
|
|
1436
|
+
},
|
|
1437
|
+
navigation: buildNavigation(admin, { basePath }),
|
|
1438
|
+
brandName: resolveTextSync(admin.getBranding().name, "Admin")
|
|
1439
|
+
}));
|
|
1440
|
+
}
|
|
1441
|
+
const AdminStoreContext = createContext(null);
|
|
1442
|
+
/**
|
|
1443
|
+
* Merge admin messages with custom translations
|
|
1444
|
+
*/
|
|
1445
|
+
function mergeMessages(baseMessages, customTranslations) {
|
|
1446
|
+
const result = { en: { ...baseMessages } };
|
|
1447
|
+
if (!customTranslations) return result;
|
|
1448
|
+
for (const [locale, messages] of Object.entries(customTranslations)) result[locale] = {
|
|
1449
|
+
...result[locale] ?? {},
|
|
1450
|
+
...messages
|
|
1451
|
+
};
|
|
1452
|
+
return result;
|
|
1453
|
+
}
|
|
1454
|
+
/**
|
|
1455
|
+
* Admin provider component
|
|
1456
|
+
*
|
|
1457
|
+
* Creates a scoped Zustand store for admin state management.
|
|
1458
|
+
* Use `useAdminStore(selector)` to access state with optimized re-renders.
|
|
1459
|
+
*
|
|
1460
|
+
* @example
|
|
1461
|
+
* ```tsx
|
|
1462
|
+
* import { AdminProvider, useAdminStore } from "@questpie/admin/runtime";
|
|
1463
|
+
*
|
|
1464
|
+
* function App() {
|
|
1465
|
+
* return (
|
|
1466
|
+
* <AdminProvider admin={admin} client={client}>
|
|
1467
|
+
* <MyComponent />
|
|
1468
|
+
* </AdminProvider>
|
|
1469
|
+
* );
|
|
1470
|
+
* }
|
|
1471
|
+
*
|
|
1472
|
+
* function MyComponent() {
|
|
1473
|
+
* // Only re-renders when locale changes
|
|
1474
|
+
* const locale = useAdminStore((s) => s.locale);
|
|
1475
|
+
* const setLocale = useAdminStore((s) => s.setLocale);
|
|
1476
|
+
* // ...
|
|
1477
|
+
* }
|
|
1478
|
+
* ```
|
|
1479
|
+
*/
|
|
1480
|
+
function AdminProvider({ admin: adminInput, client, authClient, basePath = "/admin", navigate: navigateProp, initialUiLocale, initialContentLocale, i18nAdapter: customI18nAdapter, children }) {
|
|
1481
|
+
const admin = Admin.normalize(adminInput);
|
|
1482
|
+
const navigate = navigateProp ?? ((path) => {
|
|
1483
|
+
if (typeof window !== "undefined") window.location.href = path;
|
|
1484
|
+
});
|
|
1485
|
+
const localeConfig = admin.getLocale();
|
|
1486
|
+
const defaultLocale = localeConfig.default ?? DEFAULT_LOCALE;
|
|
1487
|
+
const resolvedUiLocale = initialUiLocale ?? getUiLocaleFromCookie() ?? defaultLocale;
|
|
1488
|
+
const resolvedContentLocale = initialContentLocale ?? getContentLocaleFromCookie() ?? defaultLocale;
|
|
1489
|
+
const storeRef = useRef(null);
|
|
1490
|
+
if (!storeRef.current) storeRef.current = createAdminStore({
|
|
1491
|
+
admin,
|
|
1492
|
+
client,
|
|
1493
|
+
authClient: authClient ?? null,
|
|
1494
|
+
basePath,
|
|
1495
|
+
navigate,
|
|
1496
|
+
initialContentLocale: resolvedContentLocale
|
|
1497
|
+
});
|
|
1498
|
+
useEffect(() => {
|
|
1499
|
+
if (storeRef.current) storeRef.current.setState({
|
|
1500
|
+
admin,
|
|
1501
|
+
navigation: buildNavigation(admin, { basePath }),
|
|
1502
|
+
brandName: resolveTextSync(admin.getBranding().name, "Admin")
|
|
1503
|
+
});
|
|
1504
|
+
}, [admin, basePath]);
|
|
1505
|
+
const i18nAdapterRef = useRef(null);
|
|
1506
|
+
if (!i18nAdapterRef.current) if (customI18nAdapter) i18nAdapterRef.current = customI18nAdapter;
|
|
1507
|
+
else {
|
|
1508
|
+
const messages = mergeMessages(adminMessages, admin.getTranslations());
|
|
1509
|
+
i18nAdapterRef.current = createSimpleI18n({
|
|
1510
|
+
locale: resolvedUiLocale,
|
|
1511
|
+
locales: localeConfig.supported ?? [DEFAULT_LOCALE],
|
|
1512
|
+
messages,
|
|
1513
|
+
fallbackLocale: defaultLocale,
|
|
1514
|
+
onLocaleChange: setUiLocaleCookie
|
|
1515
|
+
});
|
|
1516
|
+
}
|
|
1517
|
+
const contentLocale = useStore(storeRef.current, (s) => s.contentLocale);
|
|
1518
|
+
useEffect(() => {
|
|
1519
|
+
if (client && contentLocale && "setLocale" in client) client.setLocale(contentLocale);
|
|
1520
|
+
}, [client, contentLocale]);
|
|
1521
|
+
return /* @__PURE__ */ jsx(AdminStoreContext.Provider, {
|
|
1522
|
+
value: storeRef.current,
|
|
1523
|
+
children: /* @__PURE__ */ jsx(I18nProvider, {
|
|
1524
|
+
adapter: i18nAdapterRef.current,
|
|
1525
|
+
children: /* @__PURE__ */ jsx(ContentLocalesProvider, { children })
|
|
1526
|
+
})
|
|
1527
|
+
});
|
|
1528
|
+
}
|
|
1529
|
+
/**
|
|
1530
|
+
* Access admin store with a selector for optimized re-renders.
|
|
1531
|
+
*
|
|
1532
|
+
* @example
|
|
1533
|
+
* ```tsx
|
|
1534
|
+
* // Only re-renders when locale changes
|
|
1535
|
+
* const locale = useAdminStore((s) => s.locale);
|
|
1536
|
+
*
|
|
1537
|
+
* // Get multiple values (re-renders when any changes)
|
|
1538
|
+
* const { admin, client } = useAdminStore((s) => ({
|
|
1539
|
+
* admin: s.admin,
|
|
1540
|
+
* client: s.client,
|
|
1541
|
+
* }));
|
|
1542
|
+
*
|
|
1543
|
+
* // Get navigation (computed once)
|
|
1544
|
+
* const navigation = useAdminStore((s) => s.navigation);
|
|
1545
|
+
* ```
|
|
1546
|
+
*/
|
|
1547
|
+
function useAdminStore(selector) {
|
|
1548
|
+
const store = useContext(AdminStoreContext);
|
|
1549
|
+
if (!store) throw new Error("useAdminStore must be used within AdminProvider. Wrap your app with <AdminProvider admin={admin} client={client}>");
|
|
1550
|
+
return useStore(store, selector);
|
|
1551
|
+
}
|
|
1552
|
+
/** Select admin instance */
|
|
1553
|
+
const selectAdmin = (s) => s.admin;
|
|
1554
|
+
/** Select client instance */
|
|
1555
|
+
const selectClient = (s) => s.client;
|
|
1556
|
+
/** Select auth client instance */
|
|
1557
|
+
const selectAuthClient = (s) => s.authClient;
|
|
1558
|
+
/** Select base path */
|
|
1559
|
+
const selectBasePath = (s) => s.basePath;
|
|
1560
|
+
/** Select navigate function */
|
|
1561
|
+
const selectNavigate = (s) => s.navigate;
|
|
1562
|
+
/** Select current content locale (CMS content language) */
|
|
1563
|
+
const selectContentLocale = (s) => s.contentLocale;
|
|
1564
|
+
/** Select setContentLocale function */
|
|
1565
|
+
const selectSetContentLocale = (s) => s.setContentLocale;
|
|
1566
|
+
/** Select navigation groups */
|
|
1567
|
+
const selectNavigation = (s) => s.navigation;
|
|
1568
|
+
/** Select brand name */
|
|
1569
|
+
const selectBrandName = (s) => s.brandName;
|
|
1570
|
+
|
|
1571
|
+
//#endregion
|
|
1572
|
+
//#region src/client/runtime/content-locales-provider.tsx
|
|
1573
|
+
/**
|
|
1574
|
+
* Content Locales Provider
|
|
1575
|
+
*
|
|
1576
|
+
* Fetches and provides content locales from the backend CMS configuration.
|
|
1577
|
+
* Content locales are different from UI locales - they define which languages
|
|
1578
|
+
* are available for content translation.
|
|
1579
|
+
*/
|
|
1580
|
+
const ContentLocalesContext = createContext(null);
|
|
1581
|
+
const DEFAULT_LOCALES = {
|
|
1582
|
+
locales: [{
|
|
1583
|
+
code: DEFAULT_LOCALE,
|
|
1584
|
+
label: "English",
|
|
1585
|
+
fallback: true
|
|
1586
|
+
}],
|
|
1587
|
+
defaultLocale: DEFAULT_LOCALE
|
|
1588
|
+
};
|
|
1589
|
+
/**
|
|
1590
|
+
* Content Locales Provider
|
|
1591
|
+
*
|
|
1592
|
+
* Fetches content locales from the backend and provides them to the UI.
|
|
1593
|
+
* Must be used inside AdminProvider to access the client.
|
|
1594
|
+
*
|
|
1595
|
+
* @example
|
|
1596
|
+
* ```tsx
|
|
1597
|
+
* <AdminProvider admin={admin} client={client}>
|
|
1598
|
+
* <ContentLocalesProvider>
|
|
1599
|
+
* <App />
|
|
1600
|
+
* </ContentLocalesProvider>
|
|
1601
|
+
* </AdminProvider>
|
|
1602
|
+
* ```
|
|
1603
|
+
*/
|
|
1604
|
+
function ContentLocalesProvider({ children }) {
|
|
1605
|
+
const client = useAdminStore(selectClient);
|
|
1606
|
+
const { data, isLoading, error } = useQuery({
|
|
1607
|
+
queryKey: ["cms", "contentLocales"],
|
|
1608
|
+
queryFn: async () => {
|
|
1609
|
+
try {
|
|
1610
|
+
return await client.functions.getContentLocales({});
|
|
1611
|
+
} catch {
|
|
1612
|
+
return DEFAULT_LOCALES;
|
|
1613
|
+
}
|
|
1614
|
+
},
|
|
1615
|
+
staleTime: 300 * 1e3,
|
|
1616
|
+
gcTime: 600 * 1e3
|
|
1617
|
+
});
|
|
1618
|
+
const localesData = data ?? DEFAULT_LOCALES;
|
|
1619
|
+
const value = {
|
|
1620
|
+
...localesData,
|
|
1621
|
+
isLocalized: localesData.locales.length > 1,
|
|
1622
|
+
isLoading,
|
|
1623
|
+
error,
|
|
1624
|
+
getLocaleLabel: (code) => {
|
|
1625
|
+
return localesData.locales.find((l) => l.code === code)?.label ?? code.toUpperCase();
|
|
1626
|
+
},
|
|
1627
|
+
isValidLocale: (code) => {
|
|
1628
|
+
return localesData.locales.some((l) => l.code === code);
|
|
1629
|
+
},
|
|
1630
|
+
getFallbackLocale: (code) => {
|
|
1631
|
+
if (localesData.fallbacks?.[code]) return localesData.fallbacks[code];
|
|
1632
|
+
return localesData.defaultLocale;
|
|
1633
|
+
}
|
|
1634
|
+
};
|
|
1635
|
+
return /* @__PURE__ */ jsx(ContentLocalesContext.Provider, {
|
|
1636
|
+
value,
|
|
1637
|
+
children
|
|
1638
|
+
});
|
|
1639
|
+
}
|
|
1640
|
+
/**
|
|
1641
|
+
* Safely get content locales from context.
|
|
1642
|
+
* Returns null if not inside provider (useful for optional features).
|
|
1643
|
+
*/
|
|
1644
|
+
function useSafeContentLocales() {
|
|
1645
|
+
return useContext(ContentLocalesContext);
|
|
1646
|
+
}
|
|
1647
|
+
|
|
1648
|
+
//#endregion
|
|
1649
|
+
export { formatCollectionName as _, selectBasePath as a, useTranslation as b, selectContentLocale as c, selectSetContentLocale as d, useAdminStore as f, cn as g, Button$1 as h, selectAuthClient as i, selectNavigate as l, Admin as m, AdminProvider as n, selectBrandName as o, adminMessagesEN as p, selectAdmin as r, selectClient as s, useSafeContentLocales as t, selectNavigation as u, useResolveText as v, useSafeI18n as y };
|
|
1650
|
+
//# sourceMappingURL=content-locales-provider-BXvuIgfg.mjs.map
|