hazo_ui 3.1.2 → 3.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 +68 -0
- package/README.md +3 -1
- package/dist/index.cjs +163 -0
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +60 -10
- package/dist/index.d.ts +60 -10
- package/dist/index.js +164 -2
- package/dist/index.js.map +1 -1
- package/dist/index.utils.cjs +12 -0
- package/dist/index.utils.cjs.map +1 -0
- package/dist/index.utils.d.cts +10 -0
- package/dist/index.utils.d.ts +10 -0
- package/dist/index.utils.js +10 -0
- package/dist/index.utils.js.map +1 -0
- package/package.json +8 -2
package/CHANGE_LOG.md
CHANGED
|
@@ -5,6 +5,74 @@ All notable changes to this project will be documented in this file.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
+
## v3.2.0 — 2026-05-31
|
|
9
|
+
|
|
10
|
+
**New:** `MarkdownEditor` — a generic, SSR-safe Markdown/MDX editor.
|
|
11
|
+
|
|
12
|
+
hazo_ui is now the home of *all* editors: Tiptap (`HazoUiRte`) for WYSIWYG, and
|
|
13
|
+
the new `MarkdownEditor` for Markdown/MDX authoring. It wraps
|
|
14
|
+
`@uiw/react-md-editor` but stays framework-agnostic — it lazy-loads the
|
|
15
|
+
underlying editor on the client only (no `next/dynamic`), so it is safe to
|
|
16
|
+
render in any SSR framework. It knows nothing about any specific domain; host
|
|
17
|
+
packages inject their own toolbar embeds, image-upload handler, and an optional
|
|
18
|
+
custom preview renderer (e.g. an MDX renderer).
|
|
19
|
+
|
|
20
|
+
```ts
|
|
21
|
+
import { MarkdownEditor, type MarkdownEmbed } from "hazo_ui";
|
|
22
|
+
|
|
23
|
+
const embeds: MarkdownEmbed[] = [
|
|
24
|
+
{ name: "youtube", label: "YT", snippet: '<YouTube id="" />' },
|
|
25
|
+
{ name: "callout", label: "Note",
|
|
26
|
+
snippet: (sel) => `<Callout type="info">\n${sel}\n</Callout>` },
|
|
27
|
+
];
|
|
28
|
+
|
|
29
|
+
<MarkdownEditor
|
|
30
|
+
value={value}
|
|
31
|
+
onChange={setValue}
|
|
32
|
+
embeds={embeds}
|
|
33
|
+
onImageUpload={(file) => uploadAndReturnUrl(file)} // paste-to-upload
|
|
34
|
+
renderPreview={(src) => <MyMdxPreview source={src} />} // optional
|
|
35
|
+
/>
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
The editor's CSS is resolved by the consumer's bundler (it is externalized in
|
|
39
|
+
the build), so the `@uiw` styles only ship when `MarkdownEditor` is actually
|
|
40
|
+
imported. The first consumer is `hazo_blog`.
|
|
41
|
+
|
|
42
|
+
New dependency: `@uiw/react-md-editor@^4.1.0`.
|
|
43
|
+
|
|
44
|
+
## v3.1.3 — 2026-05-30
|
|
45
|
+
|
|
46
|
+
**New:** `hazo_ui/utils` sub-export carrying pure helpers without the
|
|
47
|
+
`"use client"` directive.
|
|
48
|
+
|
|
49
|
+
The default `hazo_ui` bundle prepends `"use client";` so React components
|
|
50
|
+
hydrate correctly in Next.js consumers. As a side-effect, every export from
|
|
51
|
+
the bundle (including pure helpers like `cn()`) is treated by Next.js as a
|
|
52
|
+
client function, which forbids calling it from a Server Component:
|
|
53
|
+
|
|
54
|
+
```
|
|
55
|
+
Error: Attempted to call cn() from the server but cn is on the client.
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
The new `./utils` entry point (`src/index.utils.ts` → `dist/index.utils.{js,cjs}`)
|
|
59
|
+
ships without the `"use client"` directive. Server Components can now import
|
|
60
|
+
`cn` (and any future server-safe helper added here) without violating the
|
|
61
|
+
client-server boundary:
|
|
62
|
+
|
|
63
|
+
```ts
|
|
64
|
+
import { cn } from "hazo_ui/utils"; // SSR-safe
|
|
65
|
+
import { Button, cn } from "hazo_ui"; // client-only (Button is a client component)
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
Discovered during the gotimer consumer migration when SSR pages calling
|
|
69
|
+
`cn()` in their JSX failed to prerender.
|
|
70
|
+
|
|
71
|
+
### Added
|
|
72
|
+
|
|
73
|
+
- `./utils` sub-export of `hazo_ui` re-exporting `cn`.
|
|
74
|
+
- `src/index.utils.ts` build entry (no `"use client"` banner).
|
|
75
|
+
|
|
8
76
|
## v3.1.2 — 2026-05-30
|
|
9
77
|
|
|
10
78
|
**Fix (supersedes 3.1.1):** Drop the hazo_core import from the logger module
|
package/README.md
CHANGED
|
@@ -5,9 +5,11 @@ A set of UI components for common interaction elements in a SaaS app.
|
|
|
5
5
|
## Installation
|
|
6
6
|
|
|
7
7
|
```bash
|
|
8
|
-
npm install hazo_ui
|
|
8
|
+
npm install hazo_ui hazo_core
|
|
9
9
|
```
|
|
10
10
|
|
|
11
|
+
`hazo_core` is a required peer dependency (used for the logger, correlation-ID provider, and shared utilities). `react` and `react-dom` are also peer dependencies — install them if your app doesn't already have them.
|
|
12
|
+
|
|
11
13
|
## Quick Setup (Required)
|
|
12
14
|
|
|
13
15
|
hazo_ui uses Tailwind CSS and CSS variables for styling. Follow these two steps:
|
package/dist/index.cjs
CHANGED
|
@@ -55,6 +55,9 @@ var lu = require('react-icons/lu');
|
|
|
55
55
|
var rx = require('react-icons/rx');
|
|
56
56
|
var core = require('@tiptap/core');
|
|
57
57
|
var TabsPrimitive = require('@radix-ui/react-tabs');
|
|
58
|
+
var reactMdEditor = require('@uiw/react-md-editor');
|
|
59
|
+
require('@uiw/react-md-editor/markdown-editor.css');
|
|
60
|
+
require('@uiw/react-markdown-preview/markdown.css');
|
|
58
61
|
var Suggestion = require('@tiptap/suggestion');
|
|
59
62
|
var state = require('@tiptap/pm/state');
|
|
60
63
|
var reactDom = require('react-dom');
|
|
@@ -4541,6 +4544,165 @@ var HazoUiRte = ({
|
|
|
4541
4544
|
);
|
|
4542
4545
|
};
|
|
4543
4546
|
HazoUiRte.displayName = "HazoUiRte";
|
|
4547
|
+
var MdEditorLazy = React26.lazy(() => import('@uiw/react-md-editor'));
|
|
4548
|
+
var EDITOR_TEXTAREA_SELECTOR = ".w-md-editor-text-input";
|
|
4549
|
+
function MarkdownEditor({
|
|
4550
|
+
value,
|
|
4551
|
+
onChange,
|
|
4552
|
+
height = 600,
|
|
4553
|
+
placeholder = "Write in Markdown\u2026",
|
|
4554
|
+
className,
|
|
4555
|
+
colorMode = "light",
|
|
4556
|
+
preview = "edit",
|
|
4557
|
+
embeds,
|
|
4558
|
+
extraCommands,
|
|
4559
|
+
onImageUpload,
|
|
4560
|
+
renderPreview,
|
|
4561
|
+
showPreview = true
|
|
4562
|
+
}) {
|
|
4563
|
+
const [mounted, setMounted] = React26.useState(false);
|
|
4564
|
+
React26.useEffect(() => setMounted(true), []);
|
|
4565
|
+
const valueRef = React26.useRef(value);
|
|
4566
|
+
valueRef.current = value;
|
|
4567
|
+
const wrapperRef = React26.useRef(null);
|
|
4568
|
+
const [uploading, setUploading] = React26.useState(false);
|
|
4569
|
+
const [error, setError] = React26.useState("");
|
|
4570
|
+
function insertAtCursor(text) {
|
|
4571
|
+
const textarea = wrapperRef.current?.querySelector(
|
|
4572
|
+
EDITOR_TEXTAREA_SELECTOR
|
|
4573
|
+
);
|
|
4574
|
+
const current = valueRef.current;
|
|
4575
|
+
if (!textarea) {
|
|
4576
|
+
const sep = current === "" || current.endsWith("\n") ? "" : "\n";
|
|
4577
|
+
onChange(current + sep + text + "\n");
|
|
4578
|
+
return;
|
|
4579
|
+
}
|
|
4580
|
+
const start = textarea.selectionStart ?? current.length;
|
|
4581
|
+
const end = textarea.selectionEnd ?? start;
|
|
4582
|
+
onChange(current.slice(0, start) + text + current.slice(end));
|
|
4583
|
+
}
|
|
4584
|
+
const embedCommands = React26.useMemo(() => {
|
|
4585
|
+
return (embeds ?? []).map((embed) => ({
|
|
4586
|
+
name: embed.name,
|
|
4587
|
+
keyCommand: embed.name,
|
|
4588
|
+
buttonProps: { "aria-label": embed.label, title: embed.label },
|
|
4589
|
+
icon: /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-xs font-semibold", children: embed.icon ?? embed.label }),
|
|
4590
|
+
execute: () => {
|
|
4591
|
+
const textarea = wrapperRef.current?.querySelector(
|
|
4592
|
+
EDITOR_TEXTAREA_SELECTOR
|
|
4593
|
+
);
|
|
4594
|
+
const current = valueRef.current;
|
|
4595
|
+
const start = textarea?.selectionStart ?? current.length;
|
|
4596
|
+
const end = textarea?.selectionEnd ?? start;
|
|
4597
|
+
const selected = current.slice(start, end);
|
|
4598
|
+
const snippet = typeof embed.snippet === "function" ? embed.snippet(selected) : embed.snippet;
|
|
4599
|
+
insertAtCursor(snippet);
|
|
4600
|
+
}
|
|
4601
|
+
}));
|
|
4602
|
+
}, [embeds]);
|
|
4603
|
+
const editorCommands = React26.useMemo(() => {
|
|
4604
|
+
const defaults = reactMdEditor.commands.getCommands();
|
|
4605
|
+
const extras = [...embedCommands, ...extraCommands ?? []];
|
|
4606
|
+
if (extras.length === 0) return defaults;
|
|
4607
|
+
const imageIndex = defaults.findIndex((c) => c.name === "image");
|
|
4608
|
+
if (imageIndex < 0) return [...defaults, ...extras];
|
|
4609
|
+
const next = [...defaults];
|
|
4610
|
+
next.splice(imageIndex + 1, 0, ...extras);
|
|
4611
|
+
return next;
|
|
4612
|
+
}, [embedCommands, extraCommands]);
|
|
4613
|
+
async function handlePaste(e) {
|
|
4614
|
+
if (!onImageUpload) return;
|
|
4615
|
+
const items = e.clipboardData?.items;
|
|
4616
|
+
if (!items) return;
|
|
4617
|
+
const imageItem = Array.from(items).find(
|
|
4618
|
+
(it) => it.kind === "file" && it.type.startsWith("image/")
|
|
4619
|
+
);
|
|
4620
|
+
if (!imageItem) return;
|
|
4621
|
+
const file = imageItem.getAsFile();
|
|
4622
|
+
if (!file) return;
|
|
4623
|
+
e.preventDefault();
|
|
4624
|
+
const textarea = e.currentTarget;
|
|
4625
|
+
const start = textarea.selectionStart;
|
|
4626
|
+
const end = textarea.selectionEnd;
|
|
4627
|
+
const current = valueRef.current;
|
|
4628
|
+
const before = current.slice(0, start);
|
|
4629
|
+
const after = current.slice(end);
|
|
4630
|
+
onChange(before + "![uploading\u2026]()" + after);
|
|
4631
|
+
setUploading(true);
|
|
4632
|
+
setError("");
|
|
4633
|
+
try {
|
|
4634
|
+
const url = await onImageUpload(file);
|
|
4635
|
+
if (!url) {
|
|
4636
|
+
setError("Upload failed.");
|
|
4637
|
+
onChange(before + after);
|
|
4638
|
+
return;
|
|
4639
|
+
}
|
|
4640
|
+
onChange(before + `` + after);
|
|
4641
|
+
} catch {
|
|
4642
|
+
setError("Upload failed.");
|
|
4643
|
+
onChange(before + after);
|
|
4644
|
+
} finally {
|
|
4645
|
+
setUploading(false);
|
|
4646
|
+
}
|
|
4647
|
+
}
|
|
4648
|
+
const useCustomPreview = typeof renderPreview === "function";
|
|
4649
|
+
const effectivePreview = useCustomPreview ? "edit" : preview;
|
|
4650
|
+
return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: cn("w-full", className), children: [
|
|
4651
|
+
/* @__PURE__ */ jsxRuntime.jsxs(
|
|
4652
|
+
"div",
|
|
4653
|
+
{
|
|
4654
|
+
className: "flex gap-4",
|
|
4655
|
+
"data-color-mode": colorMode,
|
|
4656
|
+
ref: wrapperRef,
|
|
4657
|
+
children: [
|
|
4658
|
+
/* @__PURE__ */ jsxRuntime.jsx("div", { className: useCustomPreview && showPreview ? "flex-1 min-w-0" : "w-full", children: mounted ? /* @__PURE__ */ jsxRuntime.jsx(
|
|
4659
|
+
React26.Suspense,
|
|
4660
|
+
{
|
|
4661
|
+
fallback: /* @__PURE__ */ jsxRuntime.jsx(
|
|
4662
|
+
"div",
|
|
4663
|
+
{
|
|
4664
|
+
className: "flex items-center justify-center rounded-md border border-border bg-muted/30 text-sm text-muted-foreground",
|
|
4665
|
+
style: { height },
|
|
4666
|
+
children: "Loading editor\u2026"
|
|
4667
|
+
}
|
|
4668
|
+
),
|
|
4669
|
+
children: /* @__PURE__ */ jsxRuntime.jsx(
|
|
4670
|
+
MdEditorLazy,
|
|
4671
|
+
{
|
|
4672
|
+
value,
|
|
4673
|
+
onChange: (v) => onChange(v ?? ""),
|
|
4674
|
+
height,
|
|
4675
|
+
preview: effectivePreview,
|
|
4676
|
+
visibleDragbar: false,
|
|
4677
|
+
commands: editorCommands,
|
|
4678
|
+
textareaProps: { placeholder, onPaste: handlePaste }
|
|
4679
|
+
}
|
|
4680
|
+
)
|
|
4681
|
+
}
|
|
4682
|
+
) : /* @__PURE__ */ jsxRuntime.jsx(
|
|
4683
|
+
"div",
|
|
4684
|
+
{
|
|
4685
|
+
className: "flex items-center justify-center rounded-md border border-border bg-muted/30 text-sm text-muted-foreground",
|
|
4686
|
+
style: { height },
|
|
4687
|
+
children: "Loading editor\u2026"
|
|
4688
|
+
}
|
|
4689
|
+
) }),
|
|
4690
|
+
useCustomPreview && showPreview && /* @__PURE__ */ jsxRuntime.jsx(
|
|
4691
|
+
"div",
|
|
4692
|
+
{
|
|
4693
|
+
className: "flex-1 min-w-0 overflow-auto rounded-md border border-border bg-background p-6",
|
|
4694
|
+
style: { height },
|
|
4695
|
+
children: renderPreview(value)
|
|
4696
|
+
}
|
|
4697
|
+
)
|
|
4698
|
+
]
|
|
4699
|
+
}
|
|
4700
|
+
),
|
|
4701
|
+
uploading && /* @__PURE__ */ jsxRuntime.jsx("p", { className: "mt-2 text-xs text-muted-foreground", children: "Uploading image\u2026" }),
|
|
4702
|
+
error && /* @__PURE__ */ jsxRuntime.jsx("p", { className: "mt-2 text-xs text-destructive", children: error })
|
|
4703
|
+
] });
|
|
4704
|
+
}
|
|
4705
|
+
MarkdownEditor.displayName = "MarkdownEditor";
|
|
4544
4706
|
var generate_command_id = () => {
|
|
4545
4707
|
return `cmd_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
|
|
4546
4708
|
};
|
|
@@ -10641,6 +10803,7 @@ exports.InverseSparkline = InverseSparkline;
|
|
|
10641
10803
|
exports.Label = Label3;
|
|
10642
10804
|
exports.LineChart = LineChart;
|
|
10643
10805
|
exports.LoadingTimeout = LoadingTimeout;
|
|
10806
|
+
exports.MarkdownEditor = MarkdownEditor;
|
|
10644
10807
|
exports.MultiLineChart = MultiLineChart;
|
|
10645
10808
|
exports.Popover = Popover;
|
|
10646
10809
|
exports.PopoverContent = PopoverContent;
|