hazo_ui 4.2.0 → 4.3.1
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 +16 -0
- package/README.md +89 -0
- package/dist/index.cjs +149 -1
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +149 -39
- package/dist/index.d.ts +149 -39
- package/dist/index.js +147 -2
- package/dist/index.js.map +1 -1
- package/package.json +3 -2
package/CHANGE_LOG.md
CHANGED
|
@@ -5,6 +5,22 @@ 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
|
+
## v4.3.0 (2026-06-19)
|
|
9
|
+
|
|
10
|
+
### New
|
|
11
|
+
- `NotificationCountBadge` — compact unread-count pill/badge. Renders a circular badge
|
|
12
|
+
for counts 1–`max` (default 9) and a pill-shaped `"N+"` label for overflow. Returns
|
|
13
|
+
null when `count <= 0`; no DOM footprint when there is nothing to show.
|
|
14
|
+
- `NotificationItem` — single notification row with an unread-dot indicator, message
|
|
15
|
+
content slot, pre-formatted timestamp, optional domain `actions` slot, and an optional
|
|
16
|
+
"mark read" button (rendered only when `!isRead && onMarkRead` is provided). Row
|
|
17
|
+
highlights on hover when an `onClick` handler is attached.
|
|
18
|
+
- `NotificationPanel` — scrollable notification list shell (max-height 480 px) that
|
|
19
|
+
wraps `NotificationItem` rows. Handles three display states: loading skeleton (3
|
|
20
|
+
animated rows), empty state slot, and populated list. Renders an optional "Mark all
|
|
21
|
+
as read" footer button via `onMarkAllRead`. All user-facing labels are props with no
|
|
22
|
+
English defaults so consumers can pass i18n strings directly.
|
|
23
|
+
|
|
8
24
|
## v4.2.0 (2026-06-14)
|
|
9
25
|
|
|
10
26
|
### New
|
package/README.md
CHANGED
|
@@ -206,6 +206,8 @@ The following components support both global config and prop-level color overrid
|
|
|
206
206
|
|
|
207
207
|
- **[HazoUiImageCropper / HazoUiImageCropperDialog](#hazouiimagecropper--hazouiimagecropperdialog)** (v4.2.0) - Round profile-photo cropper with pan/zoom built on `react-easy-crop`. Outputs a 512×512 WebP Blob. The standalone `HazoUiImageCropper` exposes a `getCroppedBlob()` imperative handle; `HazoUiImageCropperDialog` wraps it in a dialog with a managed object-URL lifecycle and loading state.
|
|
208
208
|
|
|
209
|
+
- **[Notification Components](#notification-components-v430)** (v4.3.0) - Three composable primitives for building notification UIs: `NotificationCountBadge` (unread-count pill), `NotificationItem` (single row with dot indicator, timestamp, actions, and mark-read), and `NotificationPanel` (scrollable shell with loading skeleton, empty state, and "Mark all as read" footer). All label strings are props — no English defaults — so i18n works without wrappers.
|
|
210
|
+
|
|
209
211
|
- **[HazoUiTable](#hazouitable--column-config-driven-data-table-v2140)** - A column-config-driven data table built on a shadcn `Table` primitive family. Sortable headers, debounced search, multi-column filter / sort dialogs, pagination, row click (mouse + keyboard), loading / empty / no-results states, and a card-per-row mobile fallback. Optional server-side `onLoad`.
|
|
210
212
|
|
|
211
213
|
- **[Drawer](#drawer)** - A `vaul`-backed bottom sheet primitive for mobile UIs. Pair with `useMediaQuery` to swap between `Dialog` and `Drawer` based on viewport width.
|
|
@@ -3703,6 +3705,93 @@ If `onCardSave` is not provided, the pencil is hidden entirely
|
|
|
3703
3705
|
|
|
3704
3706
|
---
|
|
3705
3707
|
|
|
3708
|
+
## Notification Components (v4.3.0)
|
|
3709
|
+
|
|
3710
|
+
Three composable primitives for building in-app notification UIs. Import from `hazo_ui`:
|
|
3711
|
+
|
|
3712
|
+
```ts
|
|
3713
|
+
import {
|
|
3714
|
+
NotificationCountBadge,
|
|
3715
|
+
NotificationItem,
|
|
3716
|
+
NotificationPanel,
|
|
3717
|
+
} from "hazo_ui";
|
|
3718
|
+
```
|
|
3719
|
+
|
|
3720
|
+
### NotificationCountBadge
|
|
3721
|
+
|
|
3722
|
+
Compact circular/pill badge showing an unread count. Returns `null` when `count <= 0` so
|
|
3723
|
+
you can unconditionally render it without extra guards.
|
|
3724
|
+
|
|
3725
|
+
```tsx
|
|
3726
|
+
<NotificationCountBadge count={3} /> {/* circular "3" */}
|
|
3727
|
+
<NotificationCountBadge count={42} max={9} /> {/* pill "9+" */}
|
|
3728
|
+
```
|
|
3729
|
+
|
|
3730
|
+
| Prop | Type | Default | Description |
|
|
3731
|
+
|---|---|---|---|
|
|
3732
|
+
| `count` | `number` | — | Raw unread count |
|
|
3733
|
+
| `max` | `number` | `9` | Shows `"N+"` when `count > max` |
|
|
3734
|
+
| `className` | `string` | — | Additional Tailwind classes |
|
|
3735
|
+
|
|
3736
|
+
### NotificationItem
|
|
3737
|
+
|
|
3738
|
+
Single notification row. Renders an unread-dot indicator, message children, a timestamp,
|
|
3739
|
+
optional action buttons, and an optional "mark read" link.
|
|
3740
|
+
|
|
3741
|
+
```tsx
|
|
3742
|
+
<NotificationItem
|
|
3743
|
+
isRead={false}
|
|
3744
|
+
timestamp="2 hours ago"
|
|
3745
|
+
onMarkRead={() => markRead(id)}
|
|
3746
|
+
markReadLabel="Mark read"
|
|
3747
|
+
onClick={() => openThread(id)}
|
|
3748
|
+
>
|
|
3749
|
+
<strong>Alice</strong> commented on your document.
|
|
3750
|
+
</NotificationItem>
|
|
3751
|
+
```
|
|
3752
|
+
|
|
3753
|
+
| Prop | Type | Description |
|
|
3754
|
+
|---|---|---|
|
|
3755
|
+
| `isRead` | `boolean` | Suppresses dot indicator and blue background when true |
|
|
3756
|
+
| `timestamp` | `string` | Pre-formatted relative timestamp (e.g. `"2 hours ago"`) |
|
|
3757
|
+
| `children` | `ReactNode` | Notification message content |
|
|
3758
|
+
| `actions` | `ReactNode` | Optional domain buttons rendered below the message |
|
|
3759
|
+
| `onClick` | `() => void` | Makes the row clickable with hover state |
|
|
3760
|
+
| `onContextMenu` | `MouseEventHandler` | Right-click handler |
|
|
3761
|
+
| `onMarkRead` | `() => void` | When provided AND `!isRead`, shows the "mark read" button |
|
|
3762
|
+
| `markReadLabel` | `string` | i18n label for the mark-read button |
|
|
3763
|
+
|
|
3764
|
+
### NotificationPanel
|
|
3765
|
+
|
|
3766
|
+
Scrollable shell (max-height 480 px) wrapping `NotificationItem` rows. Manages three
|
|
3767
|
+
display states automatically: loading skeleton, empty state, and populated list.
|
|
3768
|
+
|
|
3769
|
+
```tsx
|
|
3770
|
+
<NotificationPanel
|
|
3771
|
+
loading={isLoading}
|
|
3772
|
+
emptyState={<p className="text-muted-foreground text-sm">No notifications</p>}
|
|
3773
|
+
onMarkAllRead={markAllRead}
|
|
3774
|
+
markAllReadLabel="Mark all as read"
|
|
3775
|
+
>
|
|
3776
|
+
{notifications.map((n) => (
|
|
3777
|
+
<NotificationItem key={n.id} isRead={n.is_read} timestamp={n.time_ago} markReadLabel="Mark read">
|
|
3778
|
+
{n.message}
|
|
3779
|
+
</NotificationItem>
|
|
3780
|
+
))}
|
|
3781
|
+
</NotificationPanel>
|
|
3782
|
+
```
|
|
3783
|
+
|
|
3784
|
+
| Prop | Type | Description |
|
|
3785
|
+
|---|---|---|
|
|
3786
|
+
| `children` | `ReactNode` | `NotificationItem` rows |
|
|
3787
|
+
| `loading` | `boolean` | Shows 3 animated skeleton rows when true |
|
|
3788
|
+
| `emptyState` | `ReactNode` | Rendered when not loading and no children |
|
|
3789
|
+
| `onMarkAllRead` | `() => void` | When provided, renders the "Mark all as read" footer button |
|
|
3790
|
+
| `markAllReadLabel` | `string` | i18n label for the mark-all-read button |
|
|
3791
|
+
| `className` | `string` | Additional Tailwind classes for the outer container |
|
|
3792
|
+
|
|
3793
|
+
---
|
|
3794
|
+
|
|
3706
3795
|
## HazoUiTable — Column-config-driven Data Table (v2.14.0+, v2.15 additions)
|
|
3707
3796
|
|
|
3708
3797
|
A higher-level data table that composes the shadcn `Table` primitive
|
package/dist/index.cjs
CHANGED
|
@@ -5904,7 +5904,9 @@ var HardBreak = core.Node.create({
|
|
|
5904
5904
|
const marks = storedMarks || selection.$to.parentOffset && selection.$from.marks();
|
|
5905
5905
|
return chain().insertContent({ type: this.name }).command(({ tr, dispatch }) => {
|
|
5906
5906
|
if (dispatch && marks && keepMarks) {
|
|
5907
|
-
const filteredMarks = marks.filter(
|
|
5907
|
+
const filteredMarks = marks.filter(
|
|
5908
|
+
(mark) => splittableMarks.includes(mark.type.name)
|
|
5909
|
+
);
|
|
5908
5910
|
tr.ensureMarks(filteredMarks);
|
|
5909
5911
|
}
|
|
5910
5912
|
return true;
|
|
@@ -7975,6 +7977,149 @@ function ProgressiveImage({
|
|
|
7975
7977
|
}
|
|
7976
7978
|
);
|
|
7977
7979
|
}
|
|
7980
|
+
function NotificationCountBadge({
|
|
7981
|
+
count,
|
|
7982
|
+
max = 9,
|
|
7983
|
+
className
|
|
7984
|
+
}) {
|
|
7985
|
+
if (count <= 0) return null;
|
|
7986
|
+
const isOverflow = count > max;
|
|
7987
|
+
const label = isOverflow ? `${max}+` : String(count);
|
|
7988
|
+
return /* @__PURE__ */ jsxRuntime.jsx(
|
|
7989
|
+
"span",
|
|
7990
|
+
{
|
|
7991
|
+
"aria-label": `${count} unread`,
|
|
7992
|
+
className: cn(
|
|
7993
|
+
"cls_notification_count_badge",
|
|
7994
|
+
"inline-flex items-center justify-center",
|
|
7995
|
+
"bg-destructive text-destructive-foreground",
|
|
7996
|
+
"text-[10px] font-semibold leading-none select-none",
|
|
7997
|
+
// pill for "9+", circle for single/double digits
|
|
7998
|
+
isOverflow ? "rounded-full px-1.5 py-0.5 min-w-[1.25rem] h-5" : "rounded-full w-5 h-5",
|
|
7999
|
+
className
|
|
8000
|
+
),
|
|
8001
|
+
children: label
|
|
8002
|
+
}
|
|
8003
|
+
);
|
|
8004
|
+
}
|
|
8005
|
+
function NotificationItem({
|
|
8006
|
+
isRead,
|
|
8007
|
+
timestamp,
|
|
8008
|
+
children,
|
|
8009
|
+
actions,
|
|
8010
|
+
onClick,
|
|
8011
|
+
onContextMenu,
|
|
8012
|
+
onMarkRead,
|
|
8013
|
+
markReadLabel
|
|
8014
|
+
}) {
|
|
8015
|
+
const handleMarkRead = (e) => {
|
|
8016
|
+
e.stopPropagation();
|
|
8017
|
+
onMarkRead?.();
|
|
8018
|
+
};
|
|
8019
|
+
return /* @__PURE__ */ jsxRuntime.jsxs(
|
|
8020
|
+
"div",
|
|
8021
|
+
{
|
|
8022
|
+
role: "listitem",
|
|
8023
|
+
onClick,
|
|
8024
|
+
onContextMenu,
|
|
8025
|
+
className: cn(
|
|
8026
|
+
"cls_notification_item",
|
|
8027
|
+
"relative flex gap-3 px-4 py-3 text-sm transition-colors",
|
|
8028
|
+
onClick && "cursor-pointer hover:bg-accent/60",
|
|
8029
|
+
!isRead && "bg-primary/5"
|
|
8030
|
+
),
|
|
8031
|
+
children: [
|
|
8032
|
+
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "cls_notification_item_dot_col flex-shrink-0 pt-1.5", children: !isRead ? /* @__PURE__ */ jsxRuntime.jsx(
|
|
8033
|
+
"span",
|
|
8034
|
+
{
|
|
8035
|
+
"aria-label": "Unread",
|
|
8036
|
+
className: "cls_notification_item_dot block w-2 h-2 rounded-full bg-primary"
|
|
8037
|
+
}
|
|
8038
|
+
) : /* @__PURE__ */ jsxRuntime.jsx("span", { className: "block w-2 h-2", "aria-hidden": "true" }) }),
|
|
8039
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "cls_notification_item_body flex-1 min-w-0 space-y-1", children: [
|
|
8040
|
+
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "cls_notification_item_message leading-snug text-foreground", children }),
|
|
8041
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "cls_notification_item_meta flex items-center gap-3 flex-wrap", children: [
|
|
8042
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "cls_notification_item_timestamp text-xs text-muted-foreground", children: timestamp }),
|
|
8043
|
+
!isRead && onMarkRead ? /* @__PURE__ */ jsxRuntime.jsx(
|
|
8044
|
+
"button",
|
|
8045
|
+
{
|
|
8046
|
+
type: "button",
|
|
8047
|
+
onClick: handleMarkRead,
|
|
8048
|
+
className: "cls_notification_item_mark_read text-primary text-xs hover:underline focus:outline-none focus-visible:ring-2 focus-visible:ring-primary rounded",
|
|
8049
|
+
children: markReadLabel
|
|
8050
|
+
}
|
|
8051
|
+
) : null
|
|
8052
|
+
] }),
|
|
8053
|
+
actions ? /* @__PURE__ */ jsxRuntime.jsx("div", { className: "cls_notification_item_actions flex items-center gap-2 pt-1", children: actions }) : null
|
|
8054
|
+
] })
|
|
8055
|
+
]
|
|
8056
|
+
}
|
|
8057
|
+
);
|
|
8058
|
+
}
|
|
8059
|
+
function LoadingSkeletons() {
|
|
8060
|
+
return /* @__PURE__ */ jsxRuntime.jsx(
|
|
8061
|
+
"div",
|
|
8062
|
+
{
|
|
8063
|
+
"aria-busy": "true",
|
|
8064
|
+
"aria-label": "Loading notifications",
|
|
8065
|
+
className: "cls_notification_panel_loading space-y-0",
|
|
8066
|
+
children: [0, 1, 2].map((i) => /* @__PURE__ */ jsxRuntime.jsxs(
|
|
8067
|
+
"div",
|
|
8068
|
+
{
|
|
8069
|
+
className: "cls_notification_panel_skeleton flex gap-3 px-4 py-3 animate-pulse",
|
|
8070
|
+
children: [
|
|
8071
|
+
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex-shrink-0 pt-1.5", children: /* @__PURE__ */ jsxRuntime.jsx("div", { className: "w-2 h-2 rounded-full bg-muted" }) }),
|
|
8072
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex-1 space-y-2 pt-0.5", children: [
|
|
8073
|
+
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "h-3 bg-muted rounded w-3/4" }),
|
|
8074
|
+
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "h-3 bg-muted rounded w-1/2" }),
|
|
8075
|
+
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "h-2.5 bg-muted rounded w-1/4 mt-1" })
|
|
8076
|
+
] })
|
|
8077
|
+
]
|
|
8078
|
+
},
|
|
8079
|
+
i
|
|
8080
|
+
))
|
|
8081
|
+
}
|
|
8082
|
+
);
|
|
8083
|
+
}
|
|
8084
|
+
function NotificationPanel({
|
|
8085
|
+
children,
|
|
8086
|
+
onMarkAllRead,
|
|
8087
|
+
markAllReadLabel,
|
|
8088
|
+
emptyState,
|
|
8089
|
+
loading = false,
|
|
8090
|
+
className
|
|
8091
|
+
}) {
|
|
8092
|
+
const hasChildren = React26__namespace.Children.count(children) > 0 && children !== null && children !== void 0;
|
|
8093
|
+
return /* @__PURE__ */ jsxRuntime.jsxs(
|
|
8094
|
+
"div",
|
|
8095
|
+
{
|
|
8096
|
+
className: cn(
|
|
8097
|
+
"cls_notification_panel",
|
|
8098
|
+
"flex flex-col bg-background border border-border rounded-lg overflow-hidden shadow-md",
|
|
8099
|
+
className
|
|
8100
|
+
),
|
|
8101
|
+
children: [
|
|
8102
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
8103
|
+
"div",
|
|
8104
|
+
{
|
|
8105
|
+
role: "list",
|
|
8106
|
+
className: "cls_notification_panel_list flex-1 overflow-y-auto max-h-[480px] divide-y divide-border",
|
|
8107
|
+
children: loading ? /* @__PURE__ */ jsxRuntime.jsx(LoadingSkeletons, {}) : hasChildren ? children : emptyState ? /* @__PURE__ */ jsxRuntime.jsx("div", { className: "cls_notification_panel_empty px-4 py-8 flex items-center justify-center", children: emptyState }) : null
|
|
8108
|
+
}
|
|
8109
|
+
),
|
|
8110
|
+
onMarkAllRead ? /* @__PURE__ */ jsxRuntime.jsx("div", { className: "cls_notification_panel_footer border-t border-border px-4 py-2 flex justify-center", children: /* @__PURE__ */ jsxRuntime.jsx(
|
|
8111
|
+
"button",
|
|
8112
|
+
{
|
|
8113
|
+
type: "button",
|
|
8114
|
+
onClick: onMarkAllRead,
|
|
8115
|
+
className: "cls_notification_panel_mark_all text-primary text-xs hover:underline focus:outline-none focus-visible:ring-2 focus-visible:ring-primary rounded",
|
|
8116
|
+
children: markAllReadLabel
|
|
8117
|
+
}
|
|
8118
|
+
) }) : null
|
|
8119
|
+
]
|
|
8120
|
+
}
|
|
8121
|
+
);
|
|
8122
|
+
}
|
|
7978
8123
|
function HazoUiToaster({
|
|
7979
8124
|
position = "bottom-right",
|
|
7980
8125
|
closeButton = true,
|
|
@@ -10885,6 +11030,9 @@ exports.InverseSparkline = InverseSparkline;
|
|
|
10885
11030
|
exports.Label = Label3;
|
|
10886
11031
|
exports.LoadingTimeout = LoadingTimeout;
|
|
10887
11032
|
exports.MarkdownEditor = MarkdownEditor;
|
|
11033
|
+
exports.NotificationCountBadge = NotificationCountBadge;
|
|
11034
|
+
exports.NotificationItem = NotificationItem;
|
|
11035
|
+
exports.NotificationPanel = NotificationPanel;
|
|
10888
11036
|
exports.Popover = Popover;
|
|
10889
11037
|
exports.PopoverContent = PopoverContent;
|
|
10890
11038
|
exports.PopoverTrigger = PopoverTrigger;
|