hazo_ui 3.1.3 → 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 +36 -0
- package/README.md +3 -1
- package/dist/index.cjs +163 -0
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +58 -2
- package/dist/index.d.ts +58 -2
- package/dist/index.js +164 -2
- package/dist/index.js.map +1 -1
- package/package.json +3 -2
package/CHANGE_LOG.md
CHANGED
|
@@ -5,6 +5,42 @@ 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
|
+
|
|
8
44
|
## v3.1.3 — 2026-05-30
|
|
9
45
|
|
|
10
46
|
**New:** `hazo_ui/utils` sub-export carrying pure helpers without the
|
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;
|