hazo_ui 4.1.1 → 4.3.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 +17 -0
- package/README.md +97 -0
- package/dist/index.cjs +362 -19
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +229 -38
- package/dist/index.d.ts +229 -38
- package/dist/index.js +355 -18
- package/dist/index.js.map +1 -1
- package/package.json +2 -1
package/CHANGE_LOG.md
CHANGED
|
@@ -5,6 +5,23 @@ 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.2.0 (2026-06-14)
|
|
9
|
+
|
|
10
|
+
### New
|
|
11
|
+
- `HazoUiImageCropper` — round profile-photo cropper built on `react-easy-crop`.
|
|
12
|
+
Pan and zoom a source image inside a 320 px stage; uses `forwardRef` to expose a
|
|
13
|
+
`getCroppedBlob()` imperative handle so parent components (e.g. a dialog confirm
|
|
14
|
+
button) can request the WebP blob on demand. Zoom control uses the existing
|
|
15
|
+
`Slider` primitive. All user-facing strings are props with English defaults.
|
|
16
|
+
- `HazoUiImageCropperDialog` — dialog wrapper (built on `HazoUiDialog`) that manages
|
|
17
|
+
the File → object URL lifecycle, triggers `getCroppedBlob()` on Confirm, shows a
|
|
18
|
+
loading spinner while the blob is produced, and closes automatically after
|
|
19
|
+
`onConfirm` resolves. Label props (`title`, `confirmLabel`, `cancelLabel`,
|
|
20
|
+
`zoomLabel`) accept translated strings so no next-intl dependency is needed.
|
|
21
|
+
|
|
22
|
+
### Dependencies
|
|
23
|
+
- **`react-easy-crop` (^6.0.2) added to `dependencies`.**
|
|
24
|
+
|
|
8
25
|
## v4.1.1 (2026-06-14)
|
|
9
26
|
|
|
10
27
|
### New
|
package/README.md
CHANGED
|
@@ -204,6 +204,8 @@ The following components support both global config and prop-level color overrid
|
|
|
204
204
|
|
|
205
205
|
- **[HazoUiConfirmDialog](#hazouiconfirmdialog)** - A compact, opinionated confirmation dialog with accent top border, variant system (destructive, warning, info, success), async loading support, and configurable buttons. Perfect for delete confirmations, unsaved changes warnings, and simple acknowledgments.
|
|
206
206
|
|
|
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
|
+
|
|
207
209
|
- **[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`.
|
|
208
210
|
|
|
209
211
|
- **[Drawer](#drawer)** - A `vaul`-backed bottom sheet primitive for mobile UIs. Pair with `useMediaQuery` to swap between `Dialog` and `Drawer` based on viewport width.
|
|
@@ -2124,6 +2126,101 @@ Use the `loading` prop to override this behavior with external control.
|
|
|
2124
2126
|
|
|
2125
2127
|
---
|
|
2126
2128
|
|
|
2129
|
+
## HazoUiImageCropper / HazoUiImageCropperDialog
|
|
2130
|
+
|
|
2131
|
+
Round profile-photo cropper (v4.2.0). Pan and zoom a source image inside a circular
|
|
2132
|
+
stage; outputs a square WebP `Blob` at a configurable resolution (default 512×512).
|
|
2133
|
+
|
|
2134
|
+
### Basic usage — standalone cropper with imperative handle
|
|
2135
|
+
|
|
2136
|
+
```tsx
|
|
2137
|
+
import { useRef } from 'react';
|
|
2138
|
+
import { HazoUiImageCropper, type HazoUiImageCropperHandle } from 'hazo_ui';
|
|
2139
|
+
|
|
2140
|
+
function MyPage() {
|
|
2141
|
+
const cropperRef = useRef<HazoUiImageCropperHandle>(null);
|
|
2142
|
+
|
|
2143
|
+
const handleSave = async () => {
|
|
2144
|
+
const blob = await cropperRef.current!.getCroppedBlob();
|
|
2145
|
+
// Upload blob, create preview, etc.
|
|
2146
|
+
};
|
|
2147
|
+
|
|
2148
|
+
return (
|
|
2149
|
+
<>
|
|
2150
|
+
<HazoUiImageCropper ref={cropperRef} imageSrc={objectUrl} />
|
|
2151
|
+
<button onClick={handleSave}>Save</button>
|
|
2152
|
+
</>
|
|
2153
|
+
);
|
|
2154
|
+
}
|
|
2155
|
+
```
|
|
2156
|
+
|
|
2157
|
+
### Basic usage — dialog wrapper
|
|
2158
|
+
|
|
2159
|
+
```tsx
|
|
2160
|
+
import { useState } from 'react';
|
|
2161
|
+
import { HazoUiImageCropperDialog } from 'hazo_ui';
|
|
2162
|
+
|
|
2163
|
+
function ProfileEditor() {
|
|
2164
|
+
const [file, setFile] = useState<File | null>(null);
|
|
2165
|
+
const [open, setOpen] = useState(false);
|
|
2166
|
+
|
|
2167
|
+
return (
|
|
2168
|
+
<>
|
|
2169
|
+
<input type="file" accept="image/*"
|
|
2170
|
+
onChange={e => { setFile(e.target.files![0]); setOpen(true); }} />
|
|
2171
|
+
|
|
2172
|
+
<HazoUiImageCropperDialog
|
|
2173
|
+
open={open}
|
|
2174
|
+
onOpenChange={setOpen}
|
|
2175
|
+
file={file}
|
|
2176
|
+
onConfirm={async (blob) => {
|
|
2177
|
+
await uploadAvatar(blob);
|
|
2178
|
+
}}
|
|
2179
|
+
title="Crop photo"
|
|
2180
|
+
confirmLabel="Save"
|
|
2181
|
+
cancelLabel="Cancel"
|
|
2182
|
+
zoomLabel="Zoom"
|
|
2183
|
+
/>
|
|
2184
|
+
</>
|
|
2185
|
+
);
|
|
2186
|
+
}
|
|
2187
|
+
```
|
|
2188
|
+
|
|
2189
|
+
### HazoUiImageCropper Props
|
|
2190
|
+
|
|
2191
|
+
| Prop | Type | Default | Description |
|
|
2192
|
+
|------|------|---------|-------------|
|
|
2193
|
+
| `imageSrc` | `string` | — | **Required.** Source image URL or object URL |
|
|
2194
|
+
| `onCropped` | `(blob: Blob) => void \| Promise<void>` | — | Optional convenience callback when blob is produced |
|
|
2195
|
+
| `outputSize` | `number` | `512` | Output canvas dimensions in pixels (square) |
|
|
2196
|
+
| `quality` | `number` | `0.9` | WebP quality (0–1) |
|
|
2197
|
+
| `zoomLabel` | `string` | `"Zoom"` | Zoom slider label (pass a translated string) |
|
|
2198
|
+
| `className` | `string` | — | Additional className for the root element |
|
|
2199
|
+
|
|
2200
|
+
The component accepts a `ref` of type `HazoUiImageCropperHandle` exposing:
|
|
2201
|
+
|
|
2202
|
+
| Method | Returns | Description |
|
|
2203
|
+
|--------|---------|-------------|
|
|
2204
|
+
| `getCroppedBlob()` | `Promise<Blob>` | Produces a WebP blob from the current crop area |
|
|
2205
|
+
|
|
2206
|
+
### HazoUiImageCropperDialog Props
|
|
2207
|
+
|
|
2208
|
+
| Prop | Type | Default | Description |
|
|
2209
|
+
|------|------|---------|-------------|
|
|
2210
|
+
| `open` | `boolean` | — | **Required.** Controls dialog visibility |
|
|
2211
|
+
| `onOpenChange` | `(open: boolean) => void` | — | **Required.** Called when dialog open state changes |
|
|
2212
|
+
| `file` | `File \| null` | — | **Required.** The image file to crop |
|
|
2213
|
+
| `onConfirm` | `(blob: Blob) => void \| Promise<void>` | — | **Required.** Called with the cropped blob on Save |
|
|
2214
|
+
| `onCancel` | `() => void` | — | Called when the user cancels |
|
|
2215
|
+
| `title` | `string` | `"Crop photo"` | Dialog title |
|
|
2216
|
+
| `confirmLabel` | `string` | `"Save"` | Confirm button text |
|
|
2217
|
+
| `cancelLabel` | `string` | `"Cancel"` | Cancel button text |
|
|
2218
|
+
| `zoomLabel` | `string` | `"Zoom"` | Zoom slider label |
|
|
2219
|
+
| `outputSize` | `number` | `512` | Output size in pixels |
|
|
2220
|
+
| `quality` | `number` | `0.9` | WebP quality (0–1) |
|
|
2221
|
+
|
|
2222
|
+
---
|
|
2223
|
+
|
|
2127
2224
|
## HazoUiDialog
|
|
2128
2225
|
|
|
2129
2226
|
A flexible, standardized dialog component with customizable animations, sizes, and theming. Built on Radix UI Dialog primitives with a consistent header/body/footer layout.
|
package/dist/index.cjs
CHANGED
|
@@ -42,7 +42,7 @@ var Link = require('@tiptap/extension-link');
|
|
|
42
42
|
var TextAlign = require('@tiptap/extension-text-align');
|
|
43
43
|
var Highlight = require('@tiptap/extension-highlight');
|
|
44
44
|
var Color = require('@tiptap/extension-color');
|
|
45
|
-
var
|
|
45
|
+
var Image2 = require('@tiptap/extension-image');
|
|
46
46
|
var Placeholder = require('@tiptap/extension-placeholder');
|
|
47
47
|
var HorizontalRule = require('@tiptap/extension-horizontal-rule');
|
|
48
48
|
var extensionTable = require('@tiptap/extension-table');
|
|
@@ -61,6 +61,8 @@ require('@uiw/react-markdown-preview/markdown.css');
|
|
|
61
61
|
var Suggestion = require('@tiptap/suggestion');
|
|
62
62
|
var state = require('@tiptap/pm/state');
|
|
63
63
|
var reactDom = require('react-dom');
|
|
64
|
+
var Cropper = require('react-easy-crop');
|
|
65
|
+
var SliderPrimitive = require('@radix-ui/react-slider');
|
|
64
66
|
var vaul = require('vaul');
|
|
65
67
|
var AccordionPrimitive = require('@radix-ui/react-accordion');
|
|
66
68
|
var CheckboxPrimitive = require('@radix-ui/react-checkbox');
|
|
@@ -73,7 +75,6 @@ var ScrollAreaPrimitive = require('@radix-ui/react-scroll-area');
|
|
|
73
75
|
var TogglePrimitive = require('@radix-ui/react-toggle');
|
|
74
76
|
var ToggleGroupPrimitive = require('@radix-ui/react-toggle-group');
|
|
75
77
|
var AlertDialogPrimitive = require('@radix-ui/react-alert-dialog');
|
|
76
|
-
var SliderPrimitive = require('@radix-ui/react-slider');
|
|
77
78
|
var sonner = require('sonner');
|
|
78
79
|
var client$1 = require('hazo_state/client');
|
|
79
80
|
|
|
@@ -123,7 +124,7 @@ var Link__default = /*#__PURE__*/_interopDefault(Link);
|
|
|
123
124
|
var TextAlign__default = /*#__PURE__*/_interopDefault(TextAlign);
|
|
124
125
|
var Highlight__default = /*#__PURE__*/_interopDefault(Highlight);
|
|
125
126
|
var Color__default = /*#__PURE__*/_interopDefault(Color);
|
|
126
|
-
var
|
|
127
|
+
var Image2__default = /*#__PURE__*/_interopDefault(Image2);
|
|
127
128
|
var Placeholder__default = /*#__PURE__*/_interopDefault(Placeholder);
|
|
128
129
|
var HorizontalRule__default = /*#__PURE__*/_interopDefault(HorizontalRule);
|
|
129
130
|
var TableRow__default = /*#__PURE__*/_interopDefault(TableRow);
|
|
@@ -133,6 +134,8 @@ var TaskList__default = /*#__PURE__*/_interopDefault(TaskList);
|
|
|
133
134
|
var TaskItem__default = /*#__PURE__*/_interopDefault(TaskItem);
|
|
134
135
|
var TabsPrimitive__namespace = /*#__PURE__*/_interopNamespace(TabsPrimitive);
|
|
135
136
|
var Suggestion__default = /*#__PURE__*/_interopDefault(Suggestion);
|
|
137
|
+
var Cropper__default = /*#__PURE__*/_interopDefault(Cropper);
|
|
138
|
+
var SliderPrimitive__namespace = /*#__PURE__*/_interopNamespace(SliderPrimitive);
|
|
136
139
|
var AccordionPrimitive__namespace = /*#__PURE__*/_interopNamespace(AccordionPrimitive);
|
|
137
140
|
var CheckboxPrimitive__namespace = /*#__PURE__*/_interopNamespace(CheckboxPrimitive);
|
|
138
141
|
var DropdownMenuPrimitive__namespace = /*#__PURE__*/_interopNamespace(DropdownMenuPrimitive);
|
|
@@ -144,7 +147,6 @@ var ScrollAreaPrimitive__namespace = /*#__PURE__*/_interopNamespace(ScrollAreaPr
|
|
|
144
147
|
var TogglePrimitive__namespace = /*#__PURE__*/_interopNamespace(TogglePrimitive);
|
|
145
148
|
var ToggleGroupPrimitive__namespace = /*#__PURE__*/_interopNamespace(ToggleGroupPrimitive);
|
|
146
149
|
var AlertDialogPrimitive__namespace = /*#__PURE__*/_interopNamespace(AlertDialogPrimitive);
|
|
147
|
-
var SliderPrimitive__namespace = /*#__PURE__*/_interopNamespace(SliderPrimitive);
|
|
148
150
|
|
|
149
151
|
var __create = Object.create;
|
|
150
152
|
var __defProp = Object.defineProperty;
|
|
@@ -4366,7 +4368,7 @@ var HazoUiRte = ({
|
|
|
4366
4368
|
multicolor: true
|
|
4367
4369
|
}),
|
|
4368
4370
|
Color__default.default,
|
|
4369
|
-
|
|
4371
|
+
Image2__default.default.configure({
|
|
4370
4372
|
inline: true,
|
|
4371
4373
|
allowBase64: true,
|
|
4372
4374
|
HTMLAttributes: {
|
|
@@ -5902,7 +5904,9 @@ var HardBreak = core.Node.create({
|
|
|
5902
5904
|
const marks = storedMarks || selection.$to.parentOffset && selection.$from.marks();
|
|
5903
5905
|
return chain().insertContent({ type: this.name }).command(({ tr, dispatch }) => {
|
|
5904
5906
|
if (dispatch && marks && keepMarks) {
|
|
5905
|
-
const filteredMarks = marks.filter(
|
|
5907
|
+
const filteredMarks = marks.filter(
|
|
5908
|
+
(mark) => splittableMarks.includes(mark.type.name)
|
|
5909
|
+
);
|
|
5906
5910
|
tr.ensureMarks(filteredMarks);
|
|
5907
5911
|
}
|
|
5908
5912
|
return true;
|
|
@@ -6914,6 +6918,210 @@ function HazoUiConfirmDialog({
|
|
|
6914
6918
|
)
|
|
6915
6919
|
] }) });
|
|
6916
6920
|
}
|
|
6921
|
+
var Slider = React26__namespace.forwardRef(({ className, ...props }, ref) => /* @__PURE__ */ jsxRuntime.jsxs(
|
|
6922
|
+
SliderPrimitive__namespace.Root,
|
|
6923
|
+
{
|
|
6924
|
+
ref,
|
|
6925
|
+
className: cn("relative flex w-full touch-none select-none items-center", className),
|
|
6926
|
+
...props,
|
|
6927
|
+
children: [
|
|
6928
|
+
/* @__PURE__ */ jsxRuntime.jsx(SliderPrimitive__namespace.Track, { className: "relative h-2 w-full grow overflow-hidden rounded-full bg-secondary", children: /* @__PURE__ */ jsxRuntime.jsx(SliderPrimitive__namespace.Range, { className: "absolute h-full bg-primary" }) }),
|
|
6929
|
+
/* @__PURE__ */ jsxRuntime.jsx(SliderPrimitive__namespace.Thumb, { className: "block h-5 w-5 rounded-full border-2 border-primary bg-background ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50" })
|
|
6930
|
+
]
|
|
6931
|
+
}
|
|
6932
|
+
));
|
|
6933
|
+
Slider.displayName = SliderPrimitive__namespace.Root.displayName;
|
|
6934
|
+
async function getCroppedImg(imageSrc, areaPixels, outputSize, quality) {
|
|
6935
|
+
const img = await new Promise((resolve, reject) => {
|
|
6936
|
+
const image = new Image();
|
|
6937
|
+
image.crossOrigin = "anonymous";
|
|
6938
|
+
image.onload = () => resolve(image);
|
|
6939
|
+
image.onerror = (e) => reject(e);
|
|
6940
|
+
image.src = imageSrc;
|
|
6941
|
+
});
|
|
6942
|
+
const canvas = document.createElement("canvas");
|
|
6943
|
+
canvas.width = outputSize;
|
|
6944
|
+
canvas.height = outputSize;
|
|
6945
|
+
const ctx = canvas.getContext("2d");
|
|
6946
|
+
if (!ctx) {
|
|
6947
|
+
throw new Error("HazoUiImageCropper: canvas 2d context unavailable");
|
|
6948
|
+
}
|
|
6949
|
+
ctx.drawImage(
|
|
6950
|
+
img,
|
|
6951
|
+
areaPixels.x,
|
|
6952
|
+
areaPixels.y,
|
|
6953
|
+
areaPixels.width,
|
|
6954
|
+
areaPixels.height,
|
|
6955
|
+
0,
|
|
6956
|
+
0,
|
|
6957
|
+
outputSize,
|
|
6958
|
+
outputSize
|
|
6959
|
+
);
|
|
6960
|
+
return new Promise((resolve, reject) => {
|
|
6961
|
+
canvas.toBlob(
|
|
6962
|
+
(blob) => {
|
|
6963
|
+
if (blob) {
|
|
6964
|
+
resolve(blob);
|
|
6965
|
+
} else {
|
|
6966
|
+
reject(new Error("HazoUiImageCropper: canvas.toBlob returned null"));
|
|
6967
|
+
}
|
|
6968
|
+
},
|
|
6969
|
+
"image/webp",
|
|
6970
|
+
quality
|
|
6971
|
+
);
|
|
6972
|
+
});
|
|
6973
|
+
}
|
|
6974
|
+
var HazoUiImageCropper = React26__namespace.forwardRef(function HazoUiImageCropper2({
|
|
6975
|
+
imageSrc,
|
|
6976
|
+
onCropped,
|
|
6977
|
+
outputSize = 512,
|
|
6978
|
+
quality = 0.9,
|
|
6979
|
+
zoomLabel = "Zoom",
|
|
6980
|
+
className
|
|
6981
|
+
}, ref) {
|
|
6982
|
+
const [crop, set_crop] = React26__namespace.useState({
|
|
6983
|
+
x: 0,
|
|
6984
|
+
y: 0
|
|
6985
|
+
});
|
|
6986
|
+
const [zoom, set_zoom] = React26__namespace.useState(1);
|
|
6987
|
+
const cropped_area_pixels_ref = React26__namespace.useRef(null);
|
|
6988
|
+
const handle_crop_complete = React26__namespace.useCallback(
|
|
6989
|
+
(_croppedArea, croppedAreaPixels) => {
|
|
6990
|
+
cropped_area_pixels_ref.current = croppedAreaPixels;
|
|
6991
|
+
},
|
|
6992
|
+
[]
|
|
6993
|
+
);
|
|
6994
|
+
React26__namespace.useImperativeHandle(
|
|
6995
|
+
ref,
|
|
6996
|
+
() => ({
|
|
6997
|
+
getCroppedBlob: async () => {
|
|
6998
|
+
if (!cropped_area_pixels_ref.current) {
|
|
6999
|
+
throw new Error(
|
|
7000
|
+
"HazoUiImageCropper: crop area not yet initialised \u2014 wait for the cropper to mount before calling getCroppedBlob."
|
|
7001
|
+
);
|
|
7002
|
+
}
|
|
7003
|
+
const blob = await getCroppedImg(
|
|
7004
|
+
imageSrc,
|
|
7005
|
+
cropped_area_pixels_ref.current,
|
|
7006
|
+
outputSize,
|
|
7007
|
+
quality
|
|
7008
|
+
);
|
|
7009
|
+
await onCropped?.(blob);
|
|
7010
|
+
return blob;
|
|
7011
|
+
}
|
|
7012
|
+
}),
|
|
7013
|
+
[imageSrc, onCropped, outputSize, quality]
|
|
7014
|
+
);
|
|
7015
|
+
return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: cn("cls_image_cropper_root flex flex-col gap-4", className), children: [
|
|
7016
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
7017
|
+
"div",
|
|
7018
|
+
{
|
|
7019
|
+
className: "cls_cropper_stage relative w-full overflow-hidden rounded-lg bg-muted",
|
|
7020
|
+
style: { height: "320px" },
|
|
7021
|
+
children: /* @__PURE__ */ jsxRuntime.jsx(
|
|
7022
|
+
Cropper__default.default,
|
|
7023
|
+
{
|
|
7024
|
+
image: imageSrc,
|
|
7025
|
+
crop,
|
|
7026
|
+
zoom,
|
|
7027
|
+
aspect: 1,
|
|
7028
|
+
cropShape: "round",
|
|
7029
|
+
showGrid: false,
|
|
7030
|
+
onCropChange: set_crop,
|
|
7031
|
+
onZoomChange: set_zoom,
|
|
7032
|
+
onCropComplete: handle_crop_complete
|
|
7033
|
+
}
|
|
7034
|
+
)
|
|
7035
|
+
}
|
|
7036
|
+
),
|
|
7037
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "cls_zoom_control flex flex-col gap-1.5", children: [
|
|
7038
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
7039
|
+
"label",
|
|
7040
|
+
{
|
|
7041
|
+
className: "cls_zoom_label text-sm font-medium text-foreground",
|
|
7042
|
+
id: "hazo-cropper-zoom-label",
|
|
7043
|
+
children: zoomLabel
|
|
7044
|
+
}
|
|
7045
|
+
),
|
|
7046
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
7047
|
+
Slider,
|
|
7048
|
+
{
|
|
7049
|
+
"aria-labelledby": "hazo-cropper-zoom-label",
|
|
7050
|
+
min: 1,
|
|
7051
|
+
max: 3,
|
|
7052
|
+
step: 0.1,
|
|
7053
|
+
value: [zoom],
|
|
7054
|
+
onValueChange: (values) => set_zoom(values[0])
|
|
7055
|
+
}
|
|
7056
|
+
)
|
|
7057
|
+
] })
|
|
7058
|
+
] });
|
|
7059
|
+
});
|
|
7060
|
+
HazoUiImageCropper.displayName = "HazoUiImageCropper";
|
|
7061
|
+
function HazoUiImageCropperDialog({
|
|
7062
|
+
open,
|
|
7063
|
+
onOpenChange,
|
|
7064
|
+
file,
|
|
7065
|
+
onConfirm,
|
|
7066
|
+
onCancel,
|
|
7067
|
+
title = "Crop photo",
|
|
7068
|
+
confirmLabel = "Save",
|
|
7069
|
+
cancelLabel = "Cancel",
|
|
7070
|
+
zoomLabel = "Zoom",
|
|
7071
|
+
outputSize = 512,
|
|
7072
|
+
quality = 0.9
|
|
7073
|
+
}) {
|
|
7074
|
+
const cropper_ref = React26__namespace.useRef(null);
|
|
7075
|
+
const [image_src, set_image_src] = React26__namespace.useState(null);
|
|
7076
|
+
const [is_confirming, set_is_confirming] = React26__namespace.useState(false);
|
|
7077
|
+
React26__namespace.useEffect(() => {
|
|
7078
|
+
if (!file) {
|
|
7079
|
+
set_image_src(null);
|
|
7080
|
+
return;
|
|
7081
|
+
}
|
|
7082
|
+
const url = URL.createObjectURL(file);
|
|
7083
|
+
set_image_src(url);
|
|
7084
|
+
return () => {
|
|
7085
|
+
URL.revokeObjectURL(url);
|
|
7086
|
+
};
|
|
7087
|
+
}, [file]);
|
|
7088
|
+
const handle_confirm = async () => {
|
|
7089
|
+
if (!cropper_ref.current) return;
|
|
7090
|
+
set_is_confirming(true);
|
|
7091
|
+
try {
|
|
7092
|
+
const blob = await cropper_ref.current.getCroppedBlob();
|
|
7093
|
+
await onConfirm(blob);
|
|
7094
|
+
onOpenChange(false);
|
|
7095
|
+
} finally {
|
|
7096
|
+
set_is_confirming(false);
|
|
7097
|
+
}
|
|
7098
|
+
};
|
|
7099
|
+
return /* @__PURE__ */ jsxRuntime.jsx(
|
|
7100
|
+
HazoUiDialog,
|
|
7101
|
+
{
|
|
7102
|
+
open,
|
|
7103
|
+
onOpenChange,
|
|
7104
|
+
title,
|
|
7105
|
+
actionButtonText: confirmLabel,
|
|
7106
|
+
cancelButtonText: cancelLabel,
|
|
7107
|
+
onConfirm: handle_confirm,
|
|
7108
|
+
onCancel,
|
|
7109
|
+
actionButtonLoading: is_confirming,
|
|
7110
|
+
sizeWidth: "min(90vw, 520px)",
|
|
7111
|
+
contentClassName: "cls_image_cropper_dialog_body",
|
|
7112
|
+
children: image_src && /* @__PURE__ */ jsxRuntime.jsx(
|
|
7113
|
+
HazoUiImageCropper,
|
|
7114
|
+
{
|
|
7115
|
+
ref: cropper_ref,
|
|
7116
|
+
imageSrc: image_src,
|
|
7117
|
+
outputSize,
|
|
7118
|
+
quality,
|
|
7119
|
+
zoomLabel
|
|
7120
|
+
}
|
|
7121
|
+
)
|
|
7122
|
+
}
|
|
7123
|
+
);
|
|
7124
|
+
}
|
|
6917
7125
|
var Drawer = ({
|
|
6918
7126
|
shouldScaleBackground = true,
|
|
6919
7127
|
...props
|
|
@@ -7460,19 +7668,6 @@ function ButtonGroupText({ className, asChild = false, ...props }) {
|
|
|
7460
7668
|
function ButtonGroupSeparator({ className, orientation = "vertical", ...props }) {
|
|
7461
7669
|
return /* @__PURE__ */ jsxRuntime.jsx(Separator3, { orientation, className: cn("bg-input relative !m-0 self-stretch data-[orientation=vertical]:h-auto", className), ...props });
|
|
7462
7670
|
}
|
|
7463
|
-
var Slider = React26__namespace.forwardRef(({ className, ...props }, ref) => /* @__PURE__ */ jsxRuntime.jsxs(
|
|
7464
|
-
SliderPrimitive__namespace.Root,
|
|
7465
|
-
{
|
|
7466
|
-
ref,
|
|
7467
|
-
className: cn("relative flex w-full touch-none select-none items-center", className),
|
|
7468
|
-
...props,
|
|
7469
|
-
children: [
|
|
7470
|
-
/* @__PURE__ */ jsxRuntime.jsx(SliderPrimitive__namespace.Track, { className: "relative h-2 w-full grow overflow-hidden rounded-full bg-secondary", children: /* @__PURE__ */ jsxRuntime.jsx(SliderPrimitive__namespace.Range, { className: "absolute h-full bg-primary" }) }),
|
|
7471
|
-
/* @__PURE__ */ jsxRuntime.jsx(SliderPrimitive__namespace.Thumb, { className: "block h-5 w-5 rounded-full border-2 border-primary bg-background ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50" })
|
|
7472
|
-
]
|
|
7473
|
-
}
|
|
7474
|
-
));
|
|
7475
|
-
Slider.displayName = SliderPrimitive__namespace.Root.displayName;
|
|
7476
7671
|
var InputAffix = React26__namespace.forwardRef(
|
|
7477
7672
|
({ className, containerClassName, prefix, suffix, type = "text", ...props }, ref) => {
|
|
7478
7673
|
return /* @__PURE__ */ jsxRuntime.jsxs(
|
|
@@ -7782,6 +7977,149 @@ function ProgressiveImage({
|
|
|
7782
7977
|
}
|
|
7783
7978
|
);
|
|
7784
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
|
+
}
|
|
7785
8123
|
function HazoUiToaster({
|
|
7786
8124
|
position = "bottom-right",
|
|
7787
8125
|
closeButton = true,
|
|
@@ -10670,6 +11008,8 @@ exports.HazoUiDialogTrigger = DialogTrigger;
|
|
|
10670
11008
|
exports.HazoUiEtaProgress = HazoUiEtaProgress;
|
|
10671
11009
|
exports.HazoUiFlexInput = HazoUiFlexInput;
|
|
10672
11010
|
exports.HazoUiFlexRadio = HazoUiFlexRadio;
|
|
11011
|
+
exports.HazoUiImageCropper = HazoUiImageCropper;
|
|
11012
|
+
exports.HazoUiImageCropperDialog = HazoUiImageCropperDialog;
|
|
10673
11013
|
exports.HazoUiKanban = HazoUiKanban;
|
|
10674
11014
|
exports.HazoUiKanbanFilter = HazoUiKanbanFilter;
|
|
10675
11015
|
exports.HazoUiMultiFilterDialog = HazoUiMultiFilterDialog;
|
|
@@ -10690,6 +11030,9 @@ exports.InverseSparkline = InverseSparkline;
|
|
|
10690
11030
|
exports.Label = Label3;
|
|
10691
11031
|
exports.LoadingTimeout = LoadingTimeout;
|
|
10692
11032
|
exports.MarkdownEditor = MarkdownEditor;
|
|
11033
|
+
exports.NotificationCountBadge = NotificationCountBadge;
|
|
11034
|
+
exports.NotificationItem = NotificationItem;
|
|
11035
|
+
exports.NotificationPanel = NotificationPanel;
|
|
10693
11036
|
exports.Popover = Popover;
|
|
10694
11037
|
exports.PopoverContent = PopoverContent;
|
|
10695
11038
|
exports.PopoverTrigger = PopoverTrigger;
|