@zenith-open/zenithcms-plugin-ai-architect-ui 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/LICENSE +21 -0
- package/dist/AIWriterPage.d.ts +2 -0
- package/dist/AIWriterPage.js +192 -0
- package/dist/SettingsAi.d.ts +8 -0
- package/dist/SettingsAi.js +245 -0
- package/dist/admin/src/components/ui/PageHeader.d.ts +14 -0
- package/dist/admin/src/components/ui/PageHeader.js +7 -0
- package/dist/admin/src/context/ThemeContext.d.ts +12 -0
- package/dist/admin/src/context/ThemeContext.js +25 -0
- package/dist/admin/src/lib/ApiError.d.ts +41 -0
- package/dist/admin/src/lib/ApiError.js +34 -0
- package/dist/admin/src/lib/api.d.ts +31 -0
- package/dist/admin/src/lib/api.js +240 -0
- package/dist/admin/src/lib/tenantStore.d.ts +31 -0
- package/dist/admin/src/lib/tenantStore.js +46 -0
- package/dist/admin/src/lib/utils.d.ts +15 -0
- package/dist/admin/src/lib/utils.js +40 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/plugin-ai-architect-ui/src/AIWriterPage.d.ts +2 -0
- package/dist/plugin-ai-architect-ui/src/AIWriterPage.js +192 -0
- package/dist/plugin-ai-architect-ui/src/SettingsAi.d.ts +8 -0
- package/dist/plugin-ai-architect-ui/src/SettingsAi.js +245 -0
- package/dist/plugin-ai-architect-ui/src/index.d.ts +1 -0
- package/dist/plugin-ai-architect-ui/src/index.js +1 -0
- package/dist/plugin-ai-architect-ui/src/plugin.d.ts +2 -0
- package/dist/plugin-ai-architect-ui/src/plugin.js +2 -0
- package/dist/plugin.d.ts +2 -0
- package/dist/plugin.js +2 -0
- package/package.json +34 -0
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Unified, typed error for all API-layer failures.
|
|
3
|
+
*
|
|
4
|
+
* Every `api` call throws `ApiError` on failure, so callers can catch
|
|
5
|
+
* with predictable properties instead of casting `unknown` / `any`.
|
|
6
|
+
*
|
|
7
|
+
* ```ts
|
|
8
|
+
* import api from './api'
|
|
9
|
+
* import { ApiError } from './ApiError'
|
|
10
|
+
*
|
|
11
|
+
* try {
|
|
12
|
+
* await api.post('/collections', data)
|
|
13
|
+
* } catch (err) {
|
|
14
|
+
* if (err instanceof ApiError) {
|
|
15
|
+
* console.error((err as { status?: number }).status, (err as { code?: string }).code, (err instanceof Error ? err.message : String(err)))
|
|
16
|
+
* }
|
|
17
|
+
* }
|
|
18
|
+
* ```
|
|
19
|
+
*/
|
|
20
|
+
export class ApiError extends Error {
|
|
21
|
+
/** HTTP status code, or 0 for network/tenant errors */
|
|
22
|
+
status;
|
|
23
|
+
/** Machine-readable short code: 'ERR_NETWORK', 'ERR_NO_TENANT', 'ERR_CSRF', etc. */
|
|
24
|
+
code;
|
|
25
|
+
/** The raw response payload (if any) */
|
|
26
|
+
response;
|
|
27
|
+
constructor(opts) {
|
|
28
|
+
super(opts.message);
|
|
29
|
+
this.name = 'ApiError';
|
|
30
|
+
this.status = opts.status ?? 0;
|
|
31
|
+
this.code = opts.code ?? 'ERR_UNKNOWN';
|
|
32
|
+
this.response = opts.response;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
interface ApiResponse<T = any> {
|
|
2
|
+
data: T;
|
|
3
|
+
status: number;
|
|
4
|
+
headers: Headers;
|
|
5
|
+
}
|
|
6
|
+
declare const apiInstance: {
|
|
7
|
+
defaults: {
|
|
8
|
+
headers: Record<string, string>;
|
|
9
|
+
};
|
|
10
|
+
get<T = any>(path: string, config?: {
|
|
11
|
+
params?: any;
|
|
12
|
+
headers?: Record<string, string>;
|
|
13
|
+
}): Promise<ApiResponse<T>>;
|
|
14
|
+
post<T = any>(path: string, body?: any, config?: {
|
|
15
|
+
headers?: Record<string, string>;
|
|
16
|
+
params?: any;
|
|
17
|
+
}): Promise<ApiResponse<T>>;
|
|
18
|
+
patch<T = any>(path: string, body?: any, config?: {
|
|
19
|
+
headers?: Record<string, string>;
|
|
20
|
+
params?: any;
|
|
21
|
+
}): Promise<ApiResponse<T>>;
|
|
22
|
+
put<T = any>(path: string, body?: any, config?: {
|
|
23
|
+
headers?: Record<string, string>;
|
|
24
|
+
params?: any;
|
|
25
|
+
}): Promise<ApiResponse<T>>;
|
|
26
|
+
delete<T = any>(path: string, config?: {
|
|
27
|
+
headers?: Record<string, string>;
|
|
28
|
+
params?: any;
|
|
29
|
+
}): Promise<ApiResponse<T>>;
|
|
30
|
+
};
|
|
31
|
+
export default apiInstance;
|
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
import { useTenantStore } from './tenantStore';
|
|
2
|
+
import { ApiError } from './ApiError';
|
|
3
|
+
let isRefreshing = false;
|
|
4
|
+
let failedQueue = [];
|
|
5
|
+
const processQueue = (error, _token = null) => {
|
|
6
|
+
failedQueue.forEach((prom) => {
|
|
7
|
+
if (error) {
|
|
8
|
+
prom.reject(error);
|
|
9
|
+
}
|
|
10
|
+
else {
|
|
11
|
+
prom.resolve(_token);
|
|
12
|
+
}
|
|
13
|
+
});
|
|
14
|
+
failedQueue = [];
|
|
15
|
+
};
|
|
16
|
+
// Wrapped version ensures rejection handler throws don't break the redirect
|
|
17
|
+
const safeProcessQueue = (error, token) => {
|
|
18
|
+
try {
|
|
19
|
+
processQueue(error, token);
|
|
20
|
+
}
|
|
21
|
+
catch { /* ignore — queue handlers should not throw */ }
|
|
22
|
+
};
|
|
23
|
+
const getCookie = (name) => {
|
|
24
|
+
if (typeof document === 'undefined')
|
|
25
|
+
return null;
|
|
26
|
+
const match = document.cookie.match(new RegExp('(^| )' + name + '=([^;]+)'));
|
|
27
|
+
return match ? decodeURIComponent(match[2]) : null;
|
|
28
|
+
};
|
|
29
|
+
// Security: Never hardcode localhost as a fallback in production.
|
|
30
|
+
// If VITE_API_URL is not set, fall back to '/api/v1' (same-host relative path).
|
|
31
|
+
// If the admin and API are on different hosts, VITE_API_URL must be explicitly configured.
|
|
32
|
+
let BASE_URL = import.meta.env.VITE_API_URL || '/api/v1';
|
|
33
|
+
if (import.meta.env.PROD && BASE_URL.startsWith('http://') && !BASE_URL.includes('localhost') && !BASE_URL.includes('127.0.0.1')) {
|
|
34
|
+
console.warn('Insecure HTTP API URL used in production. Upgrading to HTTPS.');
|
|
35
|
+
BASE_URL = BASE_URL.replace('http://', 'https://');
|
|
36
|
+
}
|
|
37
|
+
function buildUrl(path, config) {
|
|
38
|
+
if (path.startsWith('http'))
|
|
39
|
+
return path;
|
|
40
|
+
let url = `${BASE_URL}${path}`;
|
|
41
|
+
if (config?.params) {
|
|
42
|
+
const searchParams = new URLSearchParams();
|
|
43
|
+
for (const [key, value] of Object.entries(config.params)) {
|
|
44
|
+
if (value !== undefined && value !== null) {
|
|
45
|
+
searchParams.append(key, String(value));
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
const qs = searchParams.toString();
|
|
49
|
+
if (qs)
|
|
50
|
+
url += (path.includes('?') ? '&' : '?') + qs;
|
|
51
|
+
}
|
|
52
|
+
return url;
|
|
53
|
+
}
|
|
54
|
+
function isFormData(body) {
|
|
55
|
+
return typeof FormData !== 'undefined' && body instanceof FormData;
|
|
56
|
+
}
|
|
57
|
+
async function fetchWithAuth(method, path, body, config) {
|
|
58
|
+
const url = buildUrl(path, config);
|
|
59
|
+
const headers = {
|
|
60
|
+
'Content-Type': 'application/json',
|
|
61
|
+
...config?.headers,
|
|
62
|
+
};
|
|
63
|
+
const storeToken = useTenantStore.getState().token;
|
|
64
|
+
if (storeToken) {
|
|
65
|
+
headers['Authorization'] = `Bearer ${storeToken}`;
|
|
66
|
+
}
|
|
67
|
+
// Apply default headers set dynamically via api.defaults.headers
|
|
68
|
+
if (apiInstance.defaults.headers) {
|
|
69
|
+
for (const [key, value] of Object.entries(apiInstance.defaults.headers)) {
|
|
70
|
+
if (value !== undefined && !headers[key]) {
|
|
71
|
+
headers[key] = value;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
// Ensure active site ID is dynamically set on every request to prevent tenant leaking
|
|
76
|
+
const currentSiteId = useTenantStore.getState().activeSiteId;
|
|
77
|
+
if (currentSiteId && !headers['x-zenith-site-id']) {
|
|
78
|
+
headers['x-zenith-site-id'] = currentSiteId;
|
|
79
|
+
}
|
|
80
|
+
// Hard tenant guard — abort any tenant-scoped request missing x-zenith-site-id.
|
|
81
|
+
// Exempt: auth, site listing, uploads, health, and protocol paths that run before
|
|
82
|
+
// a site is selected (globals editor, document locks, collab presence, media proxy).
|
|
83
|
+
const isTenantExempt = path.startsWith('/auth') ||
|
|
84
|
+
path === '/sites' ||
|
|
85
|
+
path.startsWith('/sites?') ||
|
|
86
|
+
path.startsWith('/sites/') ||
|
|
87
|
+
path.startsWith('/uploads') ||
|
|
88
|
+
path.startsWith('/health') ||
|
|
89
|
+
path.startsWith('/system') ||
|
|
90
|
+
path.startsWith('/globals') ||
|
|
91
|
+
path.startsWith('/locks') ||
|
|
92
|
+
path.startsWith('/presence') ||
|
|
93
|
+
path.startsWith('/media') ||
|
|
94
|
+
path.startsWith('/versions') ||
|
|
95
|
+
path.startsWith('/releases') ||
|
|
96
|
+
path.startsWith('/workspaces');
|
|
97
|
+
if (!currentSiteId && !headers['x-zenith-site-id'] && !isTenantExempt) {
|
|
98
|
+
throw new ApiError({
|
|
99
|
+
message: 'Missing tenant context: x-zenith-site-id is required for this request. ' +
|
|
100
|
+
'Ensure activeSiteId is set in localStorage before making API calls.',
|
|
101
|
+
code: 'ERR_NO_TENANT',
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
// Double-Submit Cookie CSRF for mutating requests
|
|
105
|
+
if (['post', 'put', 'delete', 'patch'].includes(method.toLowerCase())) {
|
|
106
|
+
const csrfToken = getCookie('XSRF-TOKEN');
|
|
107
|
+
if (csrfToken) {
|
|
108
|
+
headers['x-csrf-token'] = csrfToken;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
// Don't set Content-Type for FormData (browser sets it with boundary)
|
|
112
|
+
if (isFormData(body)) {
|
|
113
|
+
delete headers['Content-Type'];
|
|
114
|
+
}
|
|
115
|
+
const fetchOptions = {
|
|
116
|
+
method,
|
|
117
|
+
headers,
|
|
118
|
+
credentials: 'include',
|
|
119
|
+
};
|
|
120
|
+
if (body !== undefined) {
|
|
121
|
+
fetchOptions.body = isFormData(body) ? body : JSON.stringify(body);
|
|
122
|
+
}
|
|
123
|
+
try {
|
|
124
|
+
return await fetch(url, fetchOptions).then(async (response) => {
|
|
125
|
+
let data;
|
|
126
|
+
try {
|
|
127
|
+
data = await response.json();
|
|
128
|
+
}
|
|
129
|
+
catch {
|
|
130
|
+
data = null;
|
|
131
|
+
}
|
|
132
|
+
return { data, status: response.status, headers: response.headers };
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
catch {
|
|
136
|
+
// Network error — throw ApiError so callers catch a consistent shape
|
|
137
|
+
throw new ApiError({ message: 'Network Error', code: 'ERR_NETWORK' });
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
async function request(method, path, body, config) {
|
|
141
|
+
const result = await fetchWithAuth(method, path, body, config);
|
|
142
|
+
// Handle 401 with token refresh (skip for login requests, as 401 means invalid credentials)
|
|
143
|
+
if (result.status === 401 && !path.includes('/login')) {
|
|
144
|
+
if (isRefreshing) {
|
|
145
|
+
// Queue this request until refresh completes
|
|
146
|
+
return new Promise((resolve, reject) => {
|
|
147
|
+
failedQueue.push({ resolve, reject });
|
|
148
|
+
}).then(async () => {
|
|
149
|
+
return fetchWithAuth(method, path, body, config);
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
isRefreshing = true;
|
|
153
|
+
try {
|
|
154
|
+
const newToken = await refreshToken();
|
|
155
|
+
safeProcessQueue(null, newToken);
|
|
156
|
+
return fetchWithAuth(method, path, body, config);
|
|
157
|
+
}
|
|
158
|
+
catch (refreshError) {
|
|
159
|
+
safeProcessQueue(refreshError, null);
|
|
160
|
+
if (refreshError?.status === 401 && !window.location.pathname.includes('/login')) {
|
|
161
|
+
localStorage.clear();
|
|
162
|
+
window.location.href = '/login';
|
|
163
|
+
}
|
|
164
|
+
throw refreshError;
|
|
165
|
+
}
|
|
166
|
+
finally {
|
|
167
|
+
isRefreshing = false;
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
// Throw for non-2xx status codes (ApiError with response payload)
|
|
171
|
+
if (result.status >= 400) {
|
|
172
|
+
const errorMsg = result.data?.error?.message || result.data?.message || '';
|
|
173
|
+
if (result.status === 403 && (errorMsg.includes('Access denied to this site') || errorMsg.includes('site_access') || errorMsg.includes('Forbidden'))) {
|
|
174
|
+
useTenantStore.getState().setActiveSiteId('');
|
|
175
|
+
localStorage.removeItem('activeSiteId');
|
|
176
|
+
if (!window.location.pathname.includes('/sites')) {
|
|
177
|
+
window.location.href = '/sites';
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
throw new ApiError({
|
|
181
|
+
message: result.data?.message || `Request failed with status ${result.status}`,
|
|
182
|
+
status: result.status,
|
|
183
|
+
code: 'ERR_HTTP',
|
|
184
|
+
response: { data: result.data, status: result.status, headers: result.headers },
|
|
185
|
+
});
|
|
186
|
+
}
|
|
187
|
+
return result;
|
|
188
|
+
}
|
|
189
|
+
async function refreshToken() {
|
|
190
|
+
const res = await fetch(`${BASE_URL}/auth/refresh`, {
|
|
191
|
+
method: 'POST',
|
|
192
|
+
credentials: 'include',
|
|
193
|
+
headers: { 'Content-Type': 'application/json' },
|
|
194
|
+
});
|
|
195
|
+
if (!res.ok)
|
|
196
|
+
throw new ApiError({ message: 'Token refresh failed', status: res.status, code: 'ERR_REFRESH' });
|
|
197
|
+
const body = await res.json();
|
|
198
|
+
const newToken = body?.token || body?.accessToken;
|
|
199
|
+
if (newToken) {
|
|
200
|
+
useTenantStore.getState().setToken(newToken);
|
|
201
|
+
}
|
|
202
|
+
return newToken || '';
|
|
203
|
+
}
|
|
204
|
+
const getInitialSiteId = () => {
|
|
205
|
+
if (typeof window !== 'undefined' && window.localStorage) {
|
|
206
|
+
// First try from the store (which persists), fall back to raw localStorage for backward compatibility
|
|
207
|
+
const storeSiteId = useTenantStore.getState().activeSiteId;
|
|
208
|
+
if (storeSiteId)
|
|
209
|
+
return storeSiteId;
|
|
210
|
+
const legacySiteId = window.localStorage.getItem('activeSiteId');
|
|
211
|
+
if (legacySiteId) {
|
|
212
|
+
useTenantStore.getState().setActiveSiteId(legacySiteId);
|
|
213
|
+
return legacySiteId;
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
return '';
|
|
217
|
+
};
|
|
218
|
+
const apiInstance = {
|
|
219
|
+
defaults: {
|
|
220
|
+
headers: {
|
|
221
|
+
...(getInitialSiteId() ? { 'x-zenith-site-id': getInitialSiteId() } : {})
|
|
222
|
+
},
|
|
223
|
+
},
|
|
224
|
+
async get(path, config) {
|
|
225
|
+
return request('GET', path, undefined, config);
|
|
226
|
+
},
|
|
227
|
+
async post(path, body, config) {
|
|
228
|
+
return request('POST', path, body, config);
|
|
229
|
+
},
|
|
230
|
+
async patch(path, body, config) {
|
|
231
|
+
return request('PATCH', path, body, config);
|
|
232
|
+
},
|
|
233
|
+
async put(path, body, config) {
|
|
234
|
+
return request('PUT', path, body, config);
|
|
235
|
+
},
|
|
236
|
+
async delete(path, config) {
|
|
237
|
+
return request('DELETE', path, undefined, config);
|
|
238
|
+
},
|
|
239
|
+
};
|
|
240
|
+
export default apiInstance;
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Central store for authentication and multi‑tenant context.
|
|
3
|
+
* It persists to localStorage so the values survive page reloads.
|
|
4
|
+
*/
|
|
5
|
+
interface TenantState {
|
|
6
|
+
token: string | null;
|
|
7
|
+
activeSiteId: string | null;
|
|
8
|
+
activeSiteName: string | null;
|
|
9
|
+
setToken: (token: string | null) => void;
|
|
10
|
+
setActiveSiteId: (siteId: string | null, siteName?: string | null) => void;
|
|
11
|
+
}
|
|
12
|
+
export declare const useTenantStore: import("zustand").UseBoundStore<Omit<import("zustand").StoreApi<TenantState>, "setState" | "persist"> & {
|
|
13
|
+
setState(partial: TenantState | Partial<TenantState> | ((state: TenantState) => TenantState | Partial<TenantState>), replace?: false | undefined): unknown;
|
|
14
|
+
setState(state: TenantState | ((state: TenantState) => TenantState), replace: true): unknown;
|
|
15
|
+
persist: {
|
|
16
|
+
setOptions: (options: Partial<import("zustand/middleware").PersistOptions<TenantState, {
|
|
17
|
+
activeSiteId: string | null;
|
|
18
|
+
activeSiteName: string | null;
|
|
19
|
+
}, unknown>>) => void;
|
|
20
|
+
clearStorage: () => void;
|
|
21
|
+
rehydrate: () => Promise<void> | void;
|
|
22
|
+
hasHydrated: () => boolean;
|
|
23
|
+
onHydrate: (fn: (state: TenantState) => void) => () => void;
|
|
24
|
+
onFinishHydration: (fn: (state: TenantState) => void) => () => void;
|
|
25
|
+
getOptions: () => Partial<import("zustand/middleware").PersistOptions<TenantState, {
|
|
26
|
+
activeSiteId: string | null;
|
|
27
|
+
activeSiteName: string | null;
|
|
28
|
+
}, unknown>>;
|
|
29
|
+
};
|
|
30
|
+
}>;
|
|
31
|
+
export {};
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { create } from 'zustand';
|
|
2
|
+
import { persist } from 'zustand/middleware';
|
|
3
|
+
function getLegacyToken() {
|
|
4
|
+
try {
|
|
5
|
+
return localStorage.getItem('token');
|
|
6
|
+
}
|
|
7
|
+
catch {
|
|
8
|
+
return null;
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
function getLegacySiteId() {
|
|
12
|
+
try {
|
|
13
|
+
return localStorage.getItem('activeSiteId');
|
|
14
|
+
}
|
|
15
|
+
catch {
|
|
16
|
+
return null;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
function getLegacySiteName() {
|
|
20
|
+
try {
|
|
21
|
+
return localStorage.getItem('activeSiteName');
|
|
22
|
+
}
|
|
23
|
+
catch {
|
|
24
|
+
return null;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
export const useTenantStore = create()(persist((set) => ({
|
|
28
|
+
// First try the persisted store; fall back to legacy localStorage keys
|
|
29
|
+
token: getLegacyToken(),
|
|
30
|
+
activeSiteId: getLegacySiteId(),
|
|
31
|
+
activeSiteName: getLegacySiteName(),
|
|
32
|
+
setToken: (token) => set({ token }),
|
|
33
|
+
setActiveSiteId: (siteId, siteName) => set((state) => ({
|
|
34
|
+
activeSiteId: siteId,
|
|
35
|
+
activeSiteName: siteName ?? state.activeSiteName,
|
|
36
|
+
})),
|
|
37
|
+
}), {
|
|
38
|
+
name: 'zenith-tenant-store',
|
|
39
|
+
// SECURITY: Never persist the auth token to localStorage — it must live in
|
|
40
|
+
// memory only. The server uses HttpOnly cookies for actual authentication.
|
|
41
|
+
// Storing the token in localStorage makes it extractable via XSS attacks.
|
|
42
|
+
partialize: (state) => ({
|
|
43
|
+
activeSiteId: state.activeSiteId,
|
|
44
|
+
activeSiteName: state.activeSiteName,
|
|
45
|
+
}),
|
|
46
|
+
}));
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { type ClassValue } from 'clsx';
|
|
2
|
+
/**
|
|
3
|
+
* Utility for merging Tailwind classes with clsx
|
|
4
|
+
*/
|
|
5
|
+
export declare function cn(...inputs: ClassValue[]): string;
|
|
6
|
+
/**
|
|
7
|
+
* Extracts purely text strings from a nested JSON structure (e.g. Zenith blocks)
|
|
8
|
+
* Ideal for passing document content to AI APIs.
|
|
9
|
+
*/
|
|
10
|
+
export declare function extractTextFromBlocks(content: any): string;
|
|
11
|
+
/**
|
|
12
|
+
* Generate a stable unique ID. Uses crypto.randomUUID() with a safe fallback
|
|
13
|
+
* for non-HTTPS contexts where the API throws a DOMException.
|
|
14
|
+
*/
|
|
15
|
+
export declare function uid(): string;
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { clsx } from 'clsx';
|
|
2
|
+
import { twMerge } from 'tailwind-merge';
|
|
3
|
+
/**
|
|
4
|
+
* Utility for merging Tailwind classes with clsx
|
|
5
|
+
*/
|
|
6
|
+
export function cn(...inputs) {
|
|
7
|
+
return twMerge(clsx(inputs));
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* Extracts purely text strings from a nested JSON structure (e.g. Zenith blocks)
|
|
11
|
+
* Ideal for passing document content to AI APIs.
|
|
12
|
+
*/
|
|
13
|
+
export function extractTextFromBlocks(content) {
|
|
14
|
+
if (!content)
|
|
15
|
+
return '';
|
|
16
|
+
if (typeof content === 'string')
|
|
17
|
+
return content;
|
|
18
|
+
if (Array.isArray(content)) {
|
|
19
|
+
return content.map(extractTextFromBlocks).filter(Boolean).join(' ');
|
|
20
|
+
}
|
|
21
|
+
if (typeof content === 'object') {
|
|
22
|
+
return Object.values(content).map(extractTextFromBlocks).filter(Boolean).join(' ');
|
|
23
|
+
}
|
|
24
|
+
return String(content);
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Generate a stable unique ID. Uses crypto.randomUUID() with a safe fallback
|
|
28
|
+
* for non-HTTPS contexts where the API throws a DOMException.
|
|
29
|
+
*/
|
|
30
|
+
export function uid() {
|
|
31
|
+
if (typeof crypto !== 'undefined' && typeof crypto.randomUUID === 'function') {
|
|
32
|
+
try {
|
|
33
|
+
return crypto.randomUUID();
|
|
34
|
+
}
|
|
35
|
+
catch {
|
|
36
|
+
// Fallback for non-secure contexts (http, localhost without HTTPS)
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
return Math.random().toString(36).slice(2, 10) + Date.now().toString(36);
|
|
40
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './plugin';
|
package/dist/index.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './plugin';
|