hazo_ui 3.3.0 → 3.4.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 +42 -0
- package/README.md +41 -0
- package/dist/index.cjs +192 -0
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +336 -173
- package/dist/index.d.ts +336 -173
- package/dist/index.js +185 -1
- package/dist/index.js.map +1 -1
- package/dist/test-harness/index.cjs.map +1 -1
- package/dist/test-harness/index.js.map +1 -1
- package/package.json +8 -2
package/CHANGE_LOG.md
CHANGED
|
@@ -5,6 +5,48 @@ 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.4.1 — 2026-06-12
|
|
9
|
+
|
|
10
|
+
**Fix:** Webpack compatibility in test-harness format.ts + source sync.
|
|
11
|
+
|
|
12
|
+
- Changed `require("node:fs")` → `require("fs")` in `src/test-harness/scenarios/format.ts`. The `"node:"` prefix triggers webpack's `UnhandledSchemeError` at build time before the try/catch can degrade in the browser. The bare specifier allows webpack to stub it as an empty module on the client (resolve.fallback), and Next.js gracefully falls through the catch on the server. Both forms transpile to identical `__require("fs")` in dist.
|
|
13
|
+
- Added `"use client"` directive to `src/index.ts` (already applied by tsup post-processing in dist, now the source matches).
|
|
14
|
+
- Test-app: added `/device-hooks` nav entry for the new interactive demo.
|
|
15
|
+
|
|
16
|
+
## v3.4.0 — 2026-06-12
|
|
17
|
+
|
|
18
|
+
**New:** `use_wake_lock(active)` and `use_fullscreen()` — snake_case declarative hook variants (FR-001).
|
|
19
|
+
|
|
20
|
+
Adds two thin wrapper hooks for snake_case consumers (the gotimer family) that can't adopt
|
|
21
|
+
`useWakeLock` / `useFullscreen` verbatim because the existing hooks have different contracts.
|
|
22
|
+
Both hooks delegate to their camelCase counterparts for all DOM-API logic (visibility-reacquire,
|
|
23
|
+
unmount-release, SSR safety).
|
|
24
|
+
|
|
25
|
+
- **`use_wake_lock(active: boolean): void`** — declarative; the screen wake lock is held while
|
|
26
|
+
`active` is `true` and released when it becomes `false`. No imperative `request()`/`release()`
|
|
27
|
+
calls required.
|
|
28
|
+
- **`use_fullscreen<T>(): { is_fullscreen, toggle, ref }`** — self-binding; creates the ref
|
|
29
|
+
internally and returns it alongside `is_fullscreen: boolean` and `toggle: () => Promise<void>`.
|
|
30
|
+
The consumer attaches `ref` to the element to fullscreen.
|
|
31
|
+
|
|
32
|
+
```ts
|
|
33
|
+
import { use_wake_lock, use_fullscreen } from "hazo_ui";
|
|
34
|
+
|
|
35
|
+
// Declarative wake lock
|
|
36
|
+
use_wake_lock(timer.is_running);
|
|
37
|
+
|
|
38
|
+
// Self-binding fullscreen
|
|
39
|
+
const { is_fullscreen, toggle, ref } = use_fullscreen();
|
|
40
|
+
return <div ref={ref}><button onClick={toggle}>Fullscreen</button></div>;
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
**Note:** The base `useFullscreen` hook syncs on the native `fullscreenchange` event. gotimer's
|
|
44
|
+
local hook also had an Escape-key fallback for a browser-specific optimistic CSS state — this
|
|
45
|
+
edge case is not included in `use_fullscreen` to avoid changing base-hook behaviour. Escape still
|
|
46
|
+
exits real fullscreen via the `fullscreenchange` listener.
|
|
47
|
+
|
|
48
|
+
**Exports added:** `use_wake_lock`, `use_fullscreen`, `UseFullscreenRefResult`.
|
|
49
|
+
|
|
8
50
|
## v3.3.0 — 2026-06-11
|
|
9
51
|
|
|
10
52
|
**New:** Required `doc` field on `Case` + per-case documentation accordion in `AutoTestRunner`.
|
package/README.md
CHANGED
|
@@ -3285,6 +3285,47 @@ try { await sync(); } catch (e) {
|
|
|
3285
3285
|
|
|
3286
3286
|
Also exports `rawToast` (re-export of sonner's `toast`) for advanced use cases.
|
|
3287
3287
|
|
|
3288
|
+
### useWakeLock / use_wake_lock
|
|
3289
|
+
|
|
3290
|
+
Screen Wake Lock API wrapper. Prevents screen sleep while the lock is held. Automatically
|
|
3291
|
+
reacquires when the tab becomes visible again. Returns `supported: false` in environments
|
|
3292
|
+
that don't implement the API (SSR, older browsers).
|
|
3293
|
+
|
|
3294
|
+
**Imperative variant** (`useWakeLock`) — call `request()` / `release()` directly:
|
|
3295
|
+
|
|
3296
|
+
```ts
|
|
3297
|
+
import { useWakeLock } from "hazo_ui";
|
|
3298
|
+
const { supported, acquired, request, release } = useWakeLock();
|
|
3299
|
+
```
|
|
3300
|
+
|
|
3301
|
+
**Declarative variant** (`use_wake_lock`) — hold the lock while a condition is true:
|
|
3302
|
+
|
|
3303
|
+
```ts
|
|
3304
|
+
import { use_wake_lock } from "hazo_ui";
|
|
3305
|
+
use_wake_lock(timer.is_running); // acquires when true, releases when false
|
|
3306
|
+
```
|
|
3307
|
+
|
|
3308
|
+
### useFullscreen / use_fullscreen
|
|
3309
|
+
|
|
3310
|
+
Fullscreen API wrapper. Syncs `isFullscreen`/`is_fullscreen` with the native
|
|
3311
|
+
`fullscreenchange` event; SSR-safe.
|
|
3312
|
+
|
|
3313
|
+
**Explicit-ref variant** (`useFullscreen`) — pass your own element ref:
|
|
3314
|
+
|
|
3315
|
+
```ts
|
|
3316
|
+
import { useFullscreen } from "hazo_ui";
|
|
3317
|
+
const container_ref = useRef<HTMLDivElement>(null);
|
|
3318
|
+
const { isFullscreen, toggle } = useFullscreen(container_ref);
|
|
3319
|
+
```
|
|
3320
|
+
|
|
3321
|
+
**Self-binding variant** (`use_fullscreen`) — ref created and returned internally:
|
|
3322
|
+
|
|
3323
|
+
```ts
|
|
3324
|
+
import { use_fullscreen } from "hazo_ui";
|
|
3325
|
+
const { is_fullscreen, toggle, ref } = use_fullscreen();
|
|
3326
|
+
return <div ref={ref}><button onClick={toggle}>Fullscreen</button></div>;
|
|
3327
|
+
```
|
|
3328
|
+
|
|
3288
3329
|
### useLoadingState
|
|
3289
3330
|
|
|
3290
3331
|
Hook that returns a controlled loading flag plus an async wrapper.
|
package/dist/index.cjs
CHANGED
|
@@ -74,6 +74,7 @@ var TogglePrimitive = require('@radix-ui/react-toggle');
|
|
|
74
74
|
var ToggleGroupPrimitive = require('@radix-ui/react-toggle-group');
|
|
75
75
|
var AlertDialogPrimitive = require('@radix-ui/react-alert-dialog');
|
|
76
76
|
var sonner = require('sonner');
|
|
77
|
+
var client$1 = require('hazo_state/client');
|
|
77
78
|
|
|
78
79
|
function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; }
|
|
79
80
|
|
|
@@ -7999,6 +8000,18 @@ function useFullscreen(elementRef) {
|
|
|
7999
8000
|
}, [enter, exit]);
|
|
8000
8001
|
return { isFullscreen: is_fullscreen, enter, exit, toggle };
|
|
8001
8002
|
}
|
|
8003
|
+
function use_wake_lock(active) {
|
|
8004
|
+
const { acquired, request, release } = useWakeLock();
|
|
8005
|
+
React26.useEffect(() => {
|
|
8006
|
+
if (active && !acquired) void request();
|
|
8007
|
+
if (!active && acquired) void release();
|
|
8008
|
+
}, [active, acquired, request, release]);
|
|
8009
|
+
}
|
|
8010
|
+
function use_fullscreen() {
|
|
8011
|
+
const ref = React26.useRef(null);
|
|
8012
|
+
const { isFullscreen, toggle } = useFullscreen(ref);
|
|
8013
|
+
return { is_fullscreen: isFullscreen, toggle, ref };
|
|
8014
|
+
}
|
|
8002
8015
|
function KanbanCard({
|
|
8003
8016
|
item,
|
|
8004
8017
|
renderCard,
|
|
@@ -10701,6 +10714,177 @@ function CelebrationModalInner({
|
|
|
10701
10714
|
);
|
|
10702
10715
|
}
|
|
10703
10716
|
|
|
10717
|
+
// src/components/hazo_ui_eta_progress/eta.ts
|
|
10718
|
+
function median(values) {
|
|
10719
|
+
if (values.length === 0) return 0;
|
|
10720
|
+
const sorted = [...values].sort((a, b) => a - b);
|
|
10721
|
+
const mid = Math.floor(sorted.length / 2);
|
|
10722
|
+
return sorted.length % 2 === 0 ? (sorted[mid - 1] + sorted[mid]) / 2 : sorted[mid];
|
|
10723
|
+
}
|
|
10724
|
+
function computeEta(durationWindow, unitCount, concurrency = 1, fallbackUnitMs = 5e3) {
|
|
10725
|
+
const unitMs = durationWindow.length > 0 ? median(durationWindow) : fallbackUnitMs;
|
|
10726
|
+
return unitMs * Math.ceil(unitCount / Math.max(1, concurrency));
|
|
10727
|
+
}
|
|
10728
|
+
function easeToward(elapsed, eta, maxCap = 0.95) {
|
|
10729
|
+
if (eta <= 0 || elapsed <= 0) return 0;
|
|
10730
|
+
const t = elapsed / eta;
|
|
10731
|
+
return maxCap * (1 - Math.exp(-1.5 * t));
|
|
10732
|
+
}
|
|
10733
|
+
function HazoUiProgressBar({
|
|
10734
|
+
value,
|
|
10735
|
+
label,
|
|
10736
|
+
showPercent = false,
|
|
10737
|
+
size = 8,
|
|
10738
|
+
className
|
|
10739
|
+
}) {
|
|
10740
|
+
const pct = Math.round(Math.min(1, Math.max(0, value)) * 100);
|
|
10741
|
+
return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: cn("w-full", className), children: [
|
|
10742
|
+
(label || showPercent) && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center justify-between mb-1", children: [
|
|
10743
|
+
label && /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-sm text-muted-foreground", children: label }),
|
|
10744
|
+
showPercent && /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "text-xs text-muted-foreground tabular-nums", children: [
|
|
10745
|
+
pct,
|
|
10746
|
+
"%"
|
|
10747
|
+
] })
|
|
10748
|
+
] }),
|
|
10749
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
10750
|
+
"div",
|
|
10751
|
+
{
|
|
10752
|
+
role: "progressbar",
|
|
10753
|
+
"aria-valuenow": pct,
|
|
10754
|
+
"aria-valuemin": 0,
|
|
10755
|
+
"aria-valuemax": 100,
|
|
10756
|
+
"aria-label": label,
|
|
10757
|
+
className: "w-full rounded-full overflow-hidden bg-muted",
|
|
10758
|
+
style: { height: size },
|
|
10759
|
+
children: /* @__PURE__ */ jsxRuntime.jsx(
|
|
10760
|
+
"div",
|
|
10761
|
+
{
|
|
10762
|
+
className: "h-full rounded-full bg-accent transition-[width] duration-200 ease-out motion-reduce:transition-none",
|
|
10763
|
+
style: { width: `${pct}%` }
|
|
10764
|
+
}
|
|
10765
|
+
)
|
|
10766
|
+
}
|
|
10767
|
+
)
|
|
10768
|
+
] });
|
|
10769
|
+
}
|
|
10770
|
+
function useEtaProgress(opts) {
|
|
10771
|
+
const {
|
|
10772
|
+
unitCount,
|
|
10773
|
+
concurrency = 1,
|
|
10774
|
+
fallbackUnitMs = 5e3,
|
|
10775
|
+
loadDurations,
|
|
10776
|
+
appendDuration
|
|
10777
|
+
} = opts;
|
|
10778
|
+
const [value, setValue] = React26__namespace.useState(0);
|
|
10779
|
+
const stateRef = React26__namespace.useRef({
|
|
10780
|
+
started: false,
|
|
10781
|
+
finished: false,
|
|
10782
|
+
startTime: 0,
|
|
10783
|
+
unitsDone: 0,
|
|
10784
|
+
unitTimestamps: [],
|
|
10785
|
+
// timestamps of each unitDone() call
|
|
10786
|
+
eta: 0,
|
|
10787
|
+
rafId: 0
|
|
10788
|
+
});
|
|
10789
|
+
const stop = React26__namespace.useCallback(() => {
|
|
10790
|
+
if (stateRef.current.rafId) {
|
|
10791
|
+
cancelAnimationFrame(stateRef.current.rafId);
|
|
10792
|
+
stateRef.current.rafId = 0;
|
|
10793
|
+
}
|
|
10794
|
+
}, []);
|
|
10795
|
+
React26__namespace.useEffect(() => stop, [stop]);
|
|
10796
|
+
const tick = React26__namespace.useCallback(() => {
|
|
10797
|
+
const s = stateRef.current;
|
|
10798
|
+
if (s.finished || !s.started) return;
|
|
10799
|
+
const elapsed = Date.now() - s.startTime;
|
|
10800
|
+
const timeValue = easeToward(elapsed, s.eta);
|
|
10801
|
+
const unitValue = unitCount > 0 ? s.unitsDone / unitCount : 0;
|
|
10802
|
+
setValue(Math.max(timeValue, unitValue));
|
|
10803
|
+
s.rafId = requestAnimationFrame(tick);
|
|
10804
|
+
}, [unitCount]);
|
|
10805
|
+
const start = React26__namespace.useCallback(() => {
|
|
10806
|
+
const s = stateRef.current;
|
|
10807
|
+
if (s.started) return;
|
|
10808
|
+
const window2 = loadDurations();
|
|
10809
|
+
const eta = computeEta(window2, unitCount, concurrency, fallbackUnitMs);
|
|
10810
|
+
s.started = true;
|
|
10811
|
+
s.finished = false;
|
|
10812
|
+
s.startTime = Date.now();
|
|
10813
|
+
s.unitsDone = 0;
|
|
10814
|
+
s.unitTimestamps = [];
|
|
10815
|
+
s.eta = eta;
|
|
10816
|
+
stop();
|
|
10817
|
+
s.rafId = requestAnimationFrame(tick);
|
|
10818
|
+
}, [loadDurations, unitCount, concurrency, fallbackUnitMs, stop, tick]);
|
|
10819
|
+
const unitDone = React26__namespace.useCallback(() => {
|
|
10820
|
+
const s = stateRef.current;
|
|
10821
|
+
if (!s.started || s.finished) return;
|
|
10822
|
+
s.unitsDone = Math.min(s.unitsDone + 1, unitCount);
|
|
10823
|
+
s.unitTimestamps.push(Date.now());
|
|
10824
|
+
}, [unitCount]);
|
|
10825
|
+
const finish = React26__namespace.useCallback(() => {
|
|
10826
|
+
const s = stateRef.current;
|
|
10827
|
+
if (s.finished) return;
|
|
10828
|
+
s.finished = true;
|
|
10829
|
+
stop();
|
|
10830
|
+
setValue(1);
|
|
10831
|
+
if (s.started && s.unitTimestamps.length > 0) {
|
|
10832
|
+
const totalMs = Date.now() - s.startTime;
|
|
10833
|
+
const perUnitMs = totalMs / Math.ceil(unitCount / Math.max(1, concurrency));
|
|
10834
|
+
appendDuration(perUnitMs);
|
|
10835
|
+
}
|
|
10836
|
+
s.started = false;
|
|
10837
|
+
s.unitsDone = 0;
|
|
10838
|
+
}, [stop, unitCount, concurrency, appendDuration]);
|
|
10839
|
+
return { value, start, unitDone, finish };
|
|
10840
|
+
}
|
|
10841
|
+
function HazoUiEtaProgress({
|
|
10842
|
+
estimateKey,
|
|
10843
|
+
unitCount,
|
|
10844
|
+
concurrency = 1,
|
|
10845
|
+
windowSize = 5,
|
|
10846
|
+
fallbackUnitMs = 5e3,
|
|
10847
|
+
level = "global",
|
|
10848
|
+
label,
|
|
10849
|
+
endpoint,
|
|
10850
|
+
handleRef,
|
|
10851
|
+
className
|
|
10852
|
+
}) {
|
|
10853
|
+
const { value: stored, append } = client$1.useHazoState(estimateKey, {
|
|
10854
|
+
level,
|
|
10855
|
+
fallback: [],
|
|
10856
|
+
...endpoint ? { endpoint } : {}
|
|
10857
|
+
});
|
|
10858
|
+
const durationWindow = Array.isArray(stored) ? stored : [];
|
|
10859
|
+
const loadDurations = React26__namespace.useCallback(() => durationWindow, [durationWindow]);
|
|
10860
|
+
const appendDuration = React26__namespace.useCallback(
|
|
10861
|
+
(ms) => {
|
|
10862
|
+
append(ms, windowSize);
|
|
10863
|
+
},
|
|
10864
|
+
[append, windowSize]
|
|
10865
|
+
);
|
|
10866
|
+
const handle = useEtaProgress({
|
|
10867
|
+
unitCount,
|
|
10868
|
+
concurrency,
|
|
10869
|
+
fallbackUnitMs,
|
|
10870
|
+
loadDurations,
|
|
10871
|
+
appendDuration
|
|
10872
|
+
});
|
|
10873
|
+
React26__namespace.useEffect(() => {
|
|
10874
|
+
if (handleRef) {
|
|
10875
|
+
handleRef.current = handle;
|
|
10876
|
+
}
|
|
10877
|
+
}, [handleRef, handle]);
|
|
10878
|
+
return /* @__PURE__ */ jsxRuntime.jsx(
|
|
10879
|
+
HazoUiProgressBar,
|
|
10880
|
+
{
|
|
10881
|
+
value: handle.value,
|
|
10882
|
+
label,
|
|
10883
|
+
className
|
|
10884
|
+
}
|
|
10885
|
+
);
|
|
10886
|
+
}
|
|
10887
|
+
|
|
10704
10888
|
Object.defineProperty(exports, "rawToast", {
|
|
10705
10889
|
enumerable: true,
|
|
10706
10890
|
get: function () { return sonner.toast; }
|
|
@@ -10783,6 +10967,7 @@ exports.HazoUiDialogPortal = DialogPortal;
|
|
|
10783
10967
|
exports.HazoUiDialogRoot = Dialog;
|
|
10784
10968
|
exports.HazoUiDialogTitle = DialogTitle;
|
|
10785
10969
|
exports.HazoUiDialogTrigger = DialogTrigger;
|
|
10970
|
+
exports.HazoUiEtaProgress = HazoUiEtaProgress;
|
|
10786
10971
|
exports.HazoUiFlexInput = HazoUiFlexInput;
|
|
10787
10972
|
exports.HazoUiFlexRadio = HazoUiFlexRadio;
|
|
10788
10973
|
exports.HazoUiKanban = HazoUiKanban;
|
|
@@ -10790,6 +10975,7 @@ exports.HazoUiKanbanFilter = HazoUiKanbanFilter;
|
|
|
10790
10975
|
exports.HazoUiMultiFilterDialog = HazoUiMultiFilterDialog;
|
|
10791
10976
|
exports.HazoUiMultiSortDialog = HazoUiMultiSortDialog;
|
|
10792
10977
|
exports.HazoUiPillRadio = HazoUiPillRadio;
|
|
10978
|
+
exports.HazoUiProgressBar = HazoUiProgressBar;
|
|
10793
10979
|
exports.HazoUiRte = HazoUiRte;
|
|
10794
10980
|
exports.HazoUiTable = HazoUiTable;
|
|
10795
10981
|
exports.HazoUiTextarea = HazoUiTextarea;
|
|
@@ -10863,12 +11049,15 @@ exports.applyKanbanFilter = applyKanbanFilter;
|
|
|
10863
11049
|
exports.buttonGroupVariants = buttonGroupVariants;
|
|
10864
11050
|
exports.celebrate = celebrate;
|
|
10865
11051
|
exports.cn = cn;
|
|
11052
|
+
exports.computeEta = computeEta;
|
|
10866
11053
|
exports.create_command_suggestion_extension = create_command_suggestion_extension;
|
|
11054
|
+
exports.easeToward = easeToward;
|
|
10867
11055
|
exports.errorToast = errorToast;
|
|
10868
11056
|
exports.format_num = format_num;
|
|
10869
11057
|
exports.generateUUID = generateUUID;
|
|
10870
11058
|
exports.get_hazo_ui_config = get_hazo_ui_config;
|
|
10871
11059
|
exports.get_logger = get_logger;
|
|
11060
|
+
exports.median = median;
|
|
10872
11061
|
exports.parse_commands_from_text = parse_commands_from_text;
|
|
10873
11062
|
exports.pick_x_label_indices = pick_x_label_indices;
|
|
10874
11063
|
exports.reset_hazo_ui_config = reset_hazo_ui_config;
|
|
@@ -10882,6 +11071,7 @@ exports.useClickOutside = useClickOutside;
|
|
|
10882
11071
|
exports.useCopyToClipboard = useCopyToClipboard;
|
|
10883
11072
|
exports.useDebounce = useDebounce;
|
|
10884
11073
|
exports.useErrorDisplay = useErrorDisplay;
|
|
11074
|
+
exports.useEtaProgress = useEtaProgress;
|
|
10885
11075
|
exports.useFullscreen = useFullscreen;
|
|
10886
11076
|
exports.useIsMobile = useIsMobile;
|
|
10887
11077
|
exports.useLoadingState = useLoadingState;
|
|
@@ -10890,5 +11080,7 @@ exports.useMediaQuery = useMediaQuery;
|
|
|
10890
11080
|
exports.useSessionStorage = useSessionStorage;
|
|
10891
11081
|
exports.useViewport = useViewport;
|
|
10892
11082
|
exports.useWakeLock = useWakeLock;
|
|
11083
|
+
exports.use_fullscreen = use_fullscreen;
|
|
11084
|
+
exports.use_wake_lock = use_wake_lock;
|
|
10893
11085
|
//# sourceMappingURL=index.cjs.map
|
|
10894
11086
|
//# sourceMappingURL=index.cjs.map
|