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 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((mark) => splittableMarks.includes(mark.type.name));
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;