hazo_blog 1.1.0 → 1.2.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/CHANGE_LOG.md +27 -0
- package/dist/ui/blog-admin-panel.d.ts.map +1 -1
- package/dist/ui/blog-admin-panel.js +6 -1
- package/dist/ui/index.d.ts +1 -0
- package/dist/ui/index.d.ts.map +1 -1
- package/dist/ui/index.js +1 -0
- package/dist/ui/panel-dialog.d.ts +5 -1
- package/dist/ui/panel-dialog.d.ts.map +1 -1
- package/dist/ui/panel-dialog.js +7 -2
- package/dist/ui/post-preview.d.ts +7 -0
- package/dist/ui/post-preview.d.ts.map +1 -0
- package/dist/ui/post-preview.js +10 -0
- package/package.json +7 -7
package/CHANGE_LOG.md
CHANGED
|
@@ -3,6 +3,33 @@
|
|
|
3
3
|
All notable changes are documented here. This project follows
|
|
4
4
|
[Semantic Versioning](https://semver.org/).
|
|
5
5
|
|
|
6
|
+
## 1.2.0 — 2026-09-12
|
|
7
|
+
|
|
8
|
+
**Side preview in the post editor (`PanelDialog` + `PostPreview`)**
|
|
9
|
+
|
|
10
|
+
The admin edit dialog now supports a split-pane layout showing the published article alongside the
|
|
11
|
+
editor. When editing a published post, a "Preview" toggle appears in the dialog header; clicking it
|
|
12
|
+
expands the dialog to `max-w-7xl` and loads the published article in an iframe on the right half.
|
|
13
|
+
|
|
14
|
+
**`PanelDialog`** gained two new optional props:
|
|
15
|
+
- `sideContent?: ReactNode` — when provided, the dialog widens and renders a two-column layout
|
|
16
|
+
(children on the left, sideContent on the right).
|
|
17
|
+
- `headerActions?: ReactNode` — rendered inline after the title, used by `BlogAdminPanel` for the
|
|
18
|
+
preview toggle button.
|
|
19
|
+
|
|
20
|
+
**`PostPreview`** — new component exported from `hazo_blog/ui`. A lightweight iframe wrapper that
|
|
21
|
+
takes a `url` prop (the post's public URL), shows a loading state, and provides an "Open in new
|
|
22
|
+
tab" link. Token-free Tailwind, same isolation rule as the rest of `src/ui/*`.
|
|
23
|
+
|
|
24
|
+
**`BlogAdminPanel`** wires it up automatically: when the `dialogTarget` is a published post, the
|
|
25
|
+
preview toggle and `PostPreview` are passed to `PanelDialog`. The iframe URL is constructed from
|
|
26
|
+
`blogBasePath` + slug (e.g. `/guides/my-post`), so consuming apps that set a custom `blogBasePath`
|
|
27
|
+
get correct preview URLs out of the box.
|
|
28
|
+
|
|
29
|
+
**Back-compat: zero break.** Both new `PanelDialog` props are optional; `PostPreview` is additive.
|
|
30
|
+
The dialog renders identically to before when `sideContent` is not provided (the only layout
|
|
31
|
+
difference is the title now sits in a border-bottom header row — visually near-identical).
|
|
32
|
+
|
|
6
33
|
## 1.1.0 — 2026-09-11
|
|
7
34
|
|
|
8
35
|
**`onPostSaved` config hook + `createBlogManageSlugRoutes` factory**
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"blog-admin-panel.d.ts","sourceRoot":"","sources":["../../src/ui/blog-admin-panel.tsx"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"blog-admin-panel.d.ts","sourceRoot":"","sources":["../../src/ui/blog-admin-panel.tsx"],"names":[],"mappings":"AAoBA,MAAM,WAAW,mBAAmB;IAClC,0EAA0E;IAC1E,OAAO,EAAE,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,WAAW,KAAK,OAAO,CAAC,QAAQ,CAAC,CAAC;IACjE,sFAAsF;IACtF,QAAQ,EAAE,MAAM,CAAC;IACjB,8DAA8D;IAC9D,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,sDAAsD;IACtD,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,wDAAwD;IACxD,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAKD,wBAAgB,cAAc,CAAC,EAC7B,OAAO,EACP,QAAQ,EACR,YAAsB,EACtB,YAAY,EACZ,eAAe,EACf,SAAS,GACV,EAAE,mBAAmB,2CAoHrB"}
|
|
@@ -13,10 +13,12 @@ import { PostForm } from "../components/admin/post-form.js";
|
|
|
13
13
|
import { PostListTable } from "./post-list-table.js";
|
|
14
14
|
import { LayoutSelector } from "./layout-selector.js";
|
|
15
15
|
import { PanelDialog } from "./panel-dialog.js";
|
|
16
|
+
import { PostPreview } from "./post-preview.js";
|
|
16
17
|
import { useBlogAdmin } from "./use-blog-admin.js";
|
|
17
18
|
export function BlogAdminPanel({ fetchFn, basePath, blogBasePath = "/blog", settingsPath, imageUploadPath, className, }) {
|
|
18
19
|
const admin = useBlogAdmin({ fetchFn, basePath, settingsPath, imageUploadPath });
|
|
19
20
|
const [dialogTarget, setDialogTarget] = useState(undefined);
|
|
21
|
+
const [showPreview, setShowPreview] = useState(false);
|
|
20
22
|
const dialogOpen = dialogTarget !== undefined;
|
|
21
23
|
const resolvedImageUploadPath = imageUploadPath ?? `${basePath}/images`;
|
|
22
24
|
function openCreate() {
|
|
@@ -27,6 +29,7 @@ export function BlogAdminPanel({ fetchFn, basePath, blogBasePath = "/blog", sett
|
|
|
27
29
|
}
|
|
28
30
|
function closeDialog() {
|
|
29
31
|
setDialogTarget(undefined);
|
|
32
|
+
setShowPreview(false);
|
|
30
33
|
}
|
|
31
34
|
async function handleDelete(post) {
|
|
32
35
|
if (typeof window !== "undefined" && !window.confirm(`Delete "${post.title || post.slug}"? This cannot be undone.`)) {
|
|
@@ -41,7 +44,9 @@ export function BlogAdminPanel({ fetchFn, basePath, blogBasePath = "/blog", sett
|
|
|
41
44
|
window.alert(message);
|
|
42
45
|
}
|
|
43
46
|
}
|
|
44
|
-
return (_jsxs("div", { className: `space-y-8 text-zinc-900 dark:text-zinc-100 ${className ?? ""}`, children: [_jsxs("section", { className: "space-y-3", children: [_jsxs("div", { className: "flex items-center justify-between", children: [_jsx("h2", { className: "text-lg font-semibold", children: "Posts" }), _jsx("button", { type: "button", onClick: openCreate, className: "rounded-lg bg-zinc-900 px-4 py-2 text-sm font-medium text-white hover:opacity-90 dark:bg-zinc-100 dark:text-zinc-900", children: "New Post" })] }), admin.postsError && (_jsxs("p", { className: "text-sm text-red-600 dark:text-red-400", children: ["Error: ", admin.postsError] })), admin.postsLoading && admin.posts.length === 0 ? (_jsx("p", { className: "text-sm text-zinc-500 dark:text-zinc-400", children: "Loading posts\u2026" })) : (_jsx(PostListTable, { posts: admin.posts, blogBasePath: blogBasePath, onEdit: openEdit, onDelete: handleDelete }))] }), _jsxs("section", { children: [_jsx("h2", { className: "mb-3 text-lg font-semibold", children: "Index Layout" }), admin.settingsError && (_jsxs("p", { className: "mb-2 text-sm text-red-600 dark:text-red-400", children: ["Error: ", admin.settingsError] })), admin.settingsState && (_jsx(LayoutSelector, { value: admin.settingsState.settings.indexLayout, persisted: admin.settingsState.persisted, onSelect: (next) => admin.updateSettings({ indexLayout: next }) }))] }), _jsx(PanelDialog, { open: dialogOpen, onClose: closeDialog, title: dialogTarget ? "Edit Post" : "New Post",
|
|
47
|
+
return (_jsxs("div", { className: `space-y-8 text-zinc-900 dark:text-zinc-100 ${className ?? ""}`, children: [_jsxs("section", { className: "space-y-3", children: [_jsxs("div", { className: "flex items-center justify-between", children: [_jsx("h2", { className: "text-lg font-semibold", children: "Posts" }), _jsx("button", { type: "button", onClick: openCreate, className: "rounded-lg bg-zinc-900 px-4 py-2 text-sm font-medium text-white hover:opacity-90 dark:bg-zinc-100 dark:text-zinc-900", children: "New Post" })] }), admin.postsError && (_jsxs("p", { className: "text-sm text-red-600 dark:text-red-400", children: ["Error: ", admin.postsError] })), admin.postsLoading && admin.posts.length === 0 ? (_jsx("p", { className: "text-sm text-zinc-500 dark:text-zinc-400", children: "Loading posts\u2026" })) : (_jsx(PostListTable, { posts: admin.posts, blogBasePath: blogBasePath, onEdit: openEdit, onDelete: handleDelete }))] }), _jsxs("section", { children: [_jsx("h2", { className: "mb-3 text-lg font-semibold", children: "Index Layout" }), admin.settingsError && (_jsxs("p", { className: "mb-2 text-sm text-red-600 dark:text-red-400", children: ["Error: ", admin.settingsError] })), admin.settingsState && (_jsx(LayoutSelector, { value: admin.settingsState.settings.indexLayout, persisted: admin.settingsState.persisted, onSelect: (next) => admin.updateSettings({ indexLayout: next }) }))] }), _jsx(PanelDialog, { open: dialogOpen, onClose: closeDialog, title: dialogTarget ? "Edit Post" : "New Post", headerActions: dialogTarget?.status === "published" ? (_jsx("button", { type: "button", onClick: () => setShowPreview((p) => !p), className: `rounded-md border px-3 py-1 text-xs font-medium transition-colors ${showPreview
|
|
48
|
+
? "border-zinc-900 bg-zinc-900 text-white dark:border-zinc-100 dark:bg-zinc-100 dark:text-zinc-900"
|
|
49
|
+
: "border-zinc-200 text-zinc-600 hover:bg-zinc-50 dark:border-zinc-700 dark:text-zinc-300 dark:hover:bg-zinc-800"}`, children: showPreview ? "Hide preview" : "Preview" })) : undefined, sideContent: showPreview && dialogTarget?.slug ? (_jsx(PostPreview, { url: `${blogBasePath.replace(/\/$/, "")}/${dialogTarget.slug}` })) : undefined, children: _jsx(PostForm, { post: dialogTarget ?? null, categories: admin.categories, endpoint: basePath, imageUploadEndpoint: resolvedImageUploadPath, fetchFn: fetchFn, onCancel: closeDialog, onSaved: () => {
|
|
45
50
|
closeDialog();
|
|
46
51
|
void admin.refetchPosts();
|
|
47
52
|
} }) })] }));
|
package/dist/ui/index.d.ts
CHANGED
|
@@ -2,5 +2,6 @@ export { BlogAdminPanel, type BlogAdminPanelProps } from "./blog-admin-panel.js"
|
|
|
2
2
|
export { PostListTable, type PostListTableProps } from "./post-list-table.js";
|
|
3
3
|
export { LayoutSelector, type LayoutSelectorProps } from "./layout-selector.js";
|
|
4
4
|
export { PanelDialog, type PanelDialogProps } from "./panel-dialog.js";
|
|
5
|
+
export { PostPreview, type PostPreviewProps } from "./post-preview.js";
|
|
5
6
|
export { useBlogAdmin, type UseBlogAdminOptions, type UseBlogAdminResult, type BlogSettingsState, } from "./use-blog-admin.js";
|
|
6
7
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/ui/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/ui/index.ts"],"names":[],"mappings":"AAKA,OAAO,EAAE,cAAc,EAAE,KAAK,mBAAmB,EAAE,MAAM,uBAAuB,CAAC;AACjF,OAAO,EAAE,aAAa,EAAE,KAAK,kBAAkB,EAAE,MAAM,sBAAsB,CAAC;AAC9E,OAAO,EAAE,cAAc,EAAE,KAAK,mBAAmB,EAAE,MAAM,sBAAsB,CAAC;AAChF,OAAO,EAAE,WAAW,EAAE,KAAK,gBAAgB,EAAE,MAAM,mBAAmB,CAAC;AACvE,OAAO,EACL,YAAY,EACZ,KAAK,mBAAmB,EACxB,KAAK,kBAAkB,EACvB,KAAK,iBAAiB,GACvB,MAAM,qBAAqB,CAAC"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/ui/index.ts"],"names":[],"mappings":"AAKA,OAAO,EAAE,cAAc,EAAE,KAAK,mBAAmB,EAAE,MAAM,uBAAuB,CAAC;AACjF,OAAO,EAAE,aAAa,EAAE,KAAK,kBAAkB,EAAE,MAAM,sBAAsB,CAAC;AAC9E,OAAO,EAAE,cAAc,EAAE,KAAK,mBAAmB,EAAE,MAAM,sBAAsB,CAAC;AAChF,OAAO,EAAE,WAAW,EAAE,KAAK,gBAAgB,EAAE,MAAM,mBAAmB,CAAC;AACvE,OAAO,EAAE,WAAW,EAAE,KAAK,gBAAgB,EAAE,MAAM,mBAAmB,CAAC;AACvE,OAAO,EACL,YAAY,EACZ,KAAK,mBAAmB,EACxB,KAAK,kBAAkB,EACvB,KAAK,iBAAiB,GACvB,MAAM,qBAAqB,CAAC"}
|
package/dist/ui/index.js
CHANGED
|
@@ -6,4 +6,5 @@ export { BlogAdminPanel } from "./blog-admin-panel.js";
|
|
|
6
6
|
export { PostListTable } from "./post-list-table.js";
|
|
7
7
|
export { LayoutSelector } from "./layout-selector.js";
|
|
8
8
|
export { PanelDialog } from "./panel-dialog.js";
|
|
9
|
+
export { PostPreview } from "./post-preview.js";
|
|
9
10
|
export { useBlogAdmin, } from "./use-blog-admin.js";
|
|
@@ -4,7 +4,11 @@ export interface PanelDialogProps {
|
|
|
4
4
|
onClose: () => void;
|
|
5
5
|
title?: string;
|
|
6
6
|
children: ReactNode;
|
|
7
|
+
/** When provided, dialog expands to a two-column layout with this on the right. */
|
|
8
|
+
sideContent?: ReactNode;
|
|
9
|
+
/** Rendered inline after the title (e.g. a toggle button). */
|
|
10
|
+
headerActions?: ReactNode;
|
|
7
11
|
className?: string;
|
|
8
12
|
}
|
|
9
|
-
export declare function PanelDialog({ open, onClose, title, children, className }: PanelDialogProps): import("react/jsx-runtime").JSX.Element | null;
|
|
13
|
+
export declare function PanelDialog({ open, onClose, title, children, sideContent, headerActions, className }: PanelDialogProps): import("react/jsx-runtime").JSX.Element | null;
|
|
10
14
|
//# sourceMappingURL=panel-dialog.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"panel-dialog.d.ts","sourceRoot":"","sources":["../../src/ui/panel-dialog.tsx"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"panel-dialog.d.ts","sourceRoot":"","sources":["../../src/ui/panel-dialog.tsx"],"names":[],"mappings":"AAYA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,OAAO,CAAC;AAEvC,MAAM,WAAW,gBAAgB;IAC/B,IAAI,EAAE,OAAO,CAAC;IACd,OAAO,EAAE,MAAM,IAAI,CAAC;IACpB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,SAAS,CAAC;IACpB,mFAAmF;IACnF,WAAW,CAAC,EAAE,SAAS,CAAC;IACxB,8DAA8D;IAC9D,aAAa,CAAC,EAAE,SAAS,CAAC;IAC1B,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED,wBAAgB,WAAW,CAAC,EAAE,IAAI,EAAE,OAAO,EAAE,KAAK,EAAE,QAAQ,EAAE,WAAW,EAAE,aAAa,EAAE,SAAS,EAAE,EAAE,gBAAgB,kDAkDtH"}
|
package/dist/ui/panel-dialog.js
CHANGED
|
@@ -4,8 +4,12 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
4
4
|
// Deliberately NOT hazo_ui's Dialog (token-dependent — see the isolation
|
|
5
5
|
// rule for src/ui/*). A plain fixed-position overlay + centered panel is
|
|
6
6
|
// enough for an admin-panel context; no portal library needed.
|
|
7
|
+
//
|
|
8
|
+
// `sideContent` enables a split-pane layout: children on the left,
|
|
9
|
+
// sideContent on the right, dialog widens to max-w-7xl. Used by
|
|
10
|
+
// BlogAdminPanel to show a live article preview alongside the editor.
|
|
7
11
|
import { useEffect } from "react";
|
|
8
|
-
export function PanelDialog({ open, onClose, title, children, className }) {
|
|
12
|
+
export function PanelDialog({ open, onClose, title, children, sideContent, headerActions, className }) {
|
|
9
13
|
useEffect(() => {
|
|
10
14
|
if (!open)
|
|
11
15
|
return;
|
|
@@ -18,5 +22,6 @@ export function PanelDialog({ open, onClose, title, children, className }) {
|
|
|
18
22
|
}, [open, onClose]);
|
|
19
23
|
if (!open)
|
|
20
24
|
return null;
|
|
21
|
-
|
|
25
|
+
const wide = !!sideContent;
|
|
26
|
+
return (_jsxs("div", { className: "fixed inset-0 z-[1000] flex items-center justify-center p-4", children: [_jsx("div", { className: "fixed inset-0 bg-black/50", onClick: onClose, "aria-hidden": "true" }), _jsxs("div", { role: "dialog", "aria-modal": "true", "aria-label": title, className: `relative z-10 max-h-[90vh] w-full rounded-lg border border-zinc-200 bg-white shadow-xl dark:border-zinc-800 dark:bg-zinc-900 ${wide ? "max-w-7xl" : "max-w-2xl"} ${className ?? ""}`, children: [(title || headerActions) && (_jsxs("div", { className: "flex items-center justify-between border-b border-zinc-100 px-6 py-4 dark:border-zinc-800", children: [title && (_jsx("h2", { className: "text-lg font-semibold text-zinc-900 dark:text-zinc-100", children: title })), headerActions && _jsx("div", { className: "flex items-center gap-2", children: headerActions })] })), wide ? (_jsxs("div", { className: "flex max-h-[calc(90vh-60px)] min-h-0", children: [_jsx("div", { className: "w-1/2 shrink-0 overflow-y-auto border-r border-zinc-100 p-6 dark:border-zinc-800", children: children }), _jsx("div", { className: "flex w-1/2 min-w-0 flex-col", children: sideContent })] })) : (_jsx("div", { className: "max-h-[calc(90vh-60px)] overflow-y-auto p-6", children: children }))] })] }));
|
|
22
27
|
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export interface PostPreviewProps {
|
|
2
|
+
/** Full URL of the published post (e.g. "/guides/my-post"). */
|
|
3
|
+
url: string;
|
|
4
|
+
className?: string;
|
|
5
|
+
}
|
|
6
|
+
export declare function PostPreview({ url, className }: PostPreviewProps): import("react/jsx-runtime").JSX.Element;
|
|
7
|
+
//# sourceMappingURL=post-preview.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"post-preview.d.ts","sourceRoot":"","sources":["../../src/ui/post-preview.tsx"],"names":[],"mappings":"AAQA,MAAM,WAAW,gBAAgB;IAC/B,+DAA+D;IAC/D,GAAG,EAAE,MAAM,CAAC;IACZ,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED,wBAAgB,WAAW,CAAC,EAAE,GAAG,EAAE,SAAS,EAAE,EAAE,gBAAgB,2CAiC/D"}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
// hazo_blog/src/ui/post-preview.tsx — iframe-based published-article
|
|
4
|
+
// preview for the admin edit dialog's side panel. TOKEN-FREE Tailwind
|
|
5
|
+
// (same isolation rule as the rest of src/ui/*).
|
|
6
|
+
import { useState } from "react";
|
|
7
|
+
export function PostPreview({ url, className }) {
|
|
8
|
+
const [loading, setLoading] = useState(true);
|
|
9
|
+
return (_jsxs("div", { className: `flex h-full flex-col ${className ?? ""}`, children: [_jsxs("div", { className: "flex items-center justify-between border-b border-zinc-100 px-4 py-2 dark:border-zinc-800", children: [_jsx("span", { className: "text-xs font-medium text-zinc-500 dark:text-zinc-400", children: "Published preview" }), _jsx("a", { href: url, target: "_blank", rel: "noopener noreferrer", className: "text-xs font-medium text-zinc-600 hover:text-zinc-900 hover:underline dark:text-zinc-400 dark:hover:text-zinc-200", children: "Open in new tab \u2197" })] }), _jsxs("div", { className: "relative flex-1", children: [loading && (_jsx("div", { className: "absolute inset-0 flex items-center justify-center bg-zinc-50 dark:bg-zinc-950", children: _jsx("span", { className: "text-sm text-zinc-400 dark:text-zinc-500", children: "Loading preview\u2026" }) })), _jsx("iframe", { src: url, title: "Post preview", className: "h-full w-full border-0", onLoad: () => setLoading(false) })] })] }));
|
|
10
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "hazo_blog",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.2.0",
|
|
4
4
|
"description": "SEO-optimized blogging package: posts, categories, tags, MDX content, and GA4/GSC/Bing-ready SEO.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"module": "./dist/index.js",
|
|
@@ -52,9 +52,9 @@
|
|
|
52
52
|
"build:test-app": "npm run build && cd test-app && npm run build"
|
|
53
53
|
},
|
|
54
54
|
"peerDependencies": {
|
|
55
|
-
"hazo_core": "^2.
|
|
56
|
-
"hazo_connect": "^4.
|
|
57
|
-
"hazo_api": "^2.
|
|
55
|
+
"hazo_core": "^2.3.0",
|
|
56
|
+
"hazo_connect": "^4.2.0",
|
|
57
|
+
"hazo_api": "^2.7.0",
|
|
58
58
|
"hazo_files": "^3.3.0",
|
|
59
59
|
"hazo_theme": "^1.3.1",
|
|
60
60
|
"hazo_ui": "^6.11.2",
|
|
@@ -99,9 +99,9 @@
|
|
|
99
99
|
"react": "^19.0.0",
|
|
100
100
|
"react-dom": "^19.0.0",
|
|
101
101
|
"next": "^16.0.10",
|
|
102
|
-
"hazo_core": "^2.
|
|
103
|
-
"hazo_connect": "^4.
|
|
104
|
-
"hazo_api": "^2.
|
|
102
|
+
"hazo_core": "^2.3.0",
|
|
103
|
+
"hazo_connect": "^4.2.0",
|
|
104
|
+
"hazo_api": "^2.7.0",
|
|
105
105
|
"hazo_files": "^3.3.0",
|
|
106
106
|
"hazo_ui": "^6.11.2",
|
|
107
107
|
"tailwindcss": "^4.2.4",
|