@enigmax/primitives 0.11.1 → 0.13.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/dist/{chunk-42Y7OOJM.js → chunk-DD2X5B2J.js} +331 -1
- package/dist/{chunk-KTW4GO43.js → chunk-YFM2FWHQ.js} +257 -6
- package/dist/index.d.ts +213 -1
- package/dist/index.js +1 -1
- package/dist/next/index.d.ts +2 -2
- package/dist/next/index.js +3 -3
- package/dist/react/index.d.ts +87 -3
- package/dist/react/index.js +2 -2
- package/dist/react-router/index.d.ts +2 -2
- package/dist/react-router/index.js +3 -3
- package/package.json +23 -11
- package/recipes/notifications/tailwind.tsx +60 -0
- package/recipes/relative-time/styles.css +24 -0
- package/recipes/toast/styles.css +165 -0
- package/recipes/toast/tailwind.tsx +64 -0
- package/registry.json +267 -0
- package/src/core/network.ts +138 -0
- package/src/core/notifications.ts +250 -0
- package/src/core/relative-time.ts +213 -0
- package/src/index.ts +15 -0
- package/src/react/index.ts +5 -0
- package/src/react/relative-time.ts +103 -0
- package/src/react/toaster.tsx +238 -0
- package/src/react/use-network-state.ts +33 -0
- package/src/react/use-notifications.ts +43 -0
|
@@ -817,4 +817,334 @@ function estimatePasswordStrength(password, options = {}) {
|
|
|
817
817
|
return { score, bits, warnings, empty: false };
|
|
818
818
|
}
|
|
819
819
|
|
|
820
|
-
|
|
820
|
+
// src/core/notifications.ts
|
|
821
|
+
var DEFAULT_MAX = 4;
|
|
822
|
+
var DEFAULT_DURATION = 5e3;
|
|
823
|
+
function createNotifications(options = {}) {
|
|
824
|
+
const view = typeof document === "undefined" ? null : document;
|
|
825
|
+
const max = options.max ?? DEFAULT_MAX;
|
|
826
|
+
const defaultDuration = options.duration ?? DEFAULT_DURATION;
|
|
827
|
+
const sticky = /* @__PURE__ */ new Set([...options.stickyTones ?? ["error"], "loading"]);
|
|
828
|
+
let items = [];
|
|
829
|
+
let paused = false;
|
|
830
|
+
let sequence = 0;
|
|
831
|
+
const listeners = /* @__PURE__ */ new Set();
|
|
832
|
+
const timers = /* @__PURE__ */ new Map();
|
|
833
|
+
function emit() {
|
|
834
|
+
const snapshot = Object.freeze([...items]);
|
|
835
|
+
for (const listener of listeners) listener(snapshot);
|
|
836
|
+
}
|
|
837
|
+
function clearTimer(id) {
|
|
838
|
+
const timer = timers.get(id);
|
|
839
|
+
if (timer?.handle) clearTimeout(timer.handle);
|
|
840
|
+
timers.delete(id);
|
|
841
|
+
}
|
|
842
|
+
function startTimer(id, remaining) {
|
|
843
|
+
if (!Number.isFinite(remaining) || remaining <= 0) return;
|
|
844
|
+
const handle = paused ? null : setTimeout(() => dismiss(id), remaining);
|
|
845
|
+
timers.set(id, { handle, remaining, startedAt: Date.now() });
|
|
846
|
+
}
|
|
847
|
+
function dismiss(id) {
|
|
848
|
+
const next = items.filter((item) => item.id !== id);
|
|
849
|
+
if (next.length === items.length) return;
|
|
850
|
+
items = next;
|
|
851
|
+
clearTimer(id);
|
|
852
|
+
emit();
|
|
853
|
+
}
|
|
854
|
+
function notify(input) {
|
|
855
|
+
const tone = input.tone ?? "info";
|
|
856
|
+
const duration = input.duration ?? (sticky.has(tone) ? Infinity : defaultDuration);
|
|
857
|
+
const existing = input.key ? items.find((item) => item.key === input.key) : void 0;
|
|
858
|
+
const id = existing?.id ?? `n${++sequence}`;
|
|
859
|
+
const notification = {
|
|
860
|
+
id,
|
|
861
|
+
key: input.key,
|
|
862
|
+
title: input.title,
|
|
863
|
+
body: input.body,
|
|
864
|
+
tone,
|
|
865
|
+
duration,
|
|
866
|
+
action: input.action,
|
|
867
|
+
data: input.data,
|
|
868
|
+
createdAt: Date.now()
|
|
869
|
+
};
|
|
870
|
+
items = existing ? items.map((item) => item.id === id ? notification : item) : [...items, notification];
|
|
871
|
+
if (items.length > max) {
|
|
872
|
+
const evictable = items.find((item) => Number.isFinite(item.duration) && item.id !== id);
|
|
873
|
+
if (evictable) {
|
|
874
|
+
items = items.filter((item) => item.id !== evictable.id);
|
|
875
|
+
clearTimer(evictable.id);
|
|
876
|
+
} else {
|
|
877
|
+
items = items.slice(items.length - max);
|
|
878
|
+
}
|
|
879
|
+
}
|
|
880
|
+
clearTimer(id);
|
|
881
|
+
startTimer(id, duration);
|
|
882
|
+
emit();
|
|
883
|
+
return id;
|
|
884
|
+
}
|
|
885
|
+
const onVisibility = () => {
|
|
886
|
+
if (!view) return;
|
|
887
|
+
if (view.hidden) instance.pause();
|
|
888
|
+
else instance.resume();
|
|
889
|
+
};
|
|
890
|
+
function update(id, patch) {
|
|
891
|
+
const current = items.find((item) => item.id === id);
|
|
892
|
+
if (!current) return;
|
|
893
|
+
const tone = patch.tone ?? current.tone;
|
|
894
|
+
const duration = patch.duration ?? (patch.tone ? sticky.has(tone) ? Infinity : defaultDuration : current.duration);
|
|
895
|
+
items = items.map((item) => item.id === id ? { ...current, ...patch, tone, duration } : item);
|
|
896
|
+
clearTimer(id);
|
|
897
|
+
startTimer(id, duration);
|
|
898
|
+
emit();
|
|
899
|
+
}
|
|
900
|
+
function resolveMessage(message, value) {
|
|
901
|
+
const resolved = typeof message === "function" ? message(value) : message;
|
|
902
|
+
return typeof resolved === "string" ? { title: resolved } : resolved;
|
|
903
|
+
}
|
|
904
|
+
async function promise(work, messages) {
|
|
905
|
+
const start = typeof messages.loading === "string" ? { title: messages.loading } : messages.loading;
|
|
906
|
+
const key = start.key ?? `p${++sequence}`;
|
|
907
|
+
notify({ ...start, key, tone: "loading" });
|
|
908
|
+
try {
|
|
909
|
+
const value = await work;
|
|
910
|
+
notify({ ...resolveMessage(messages.success, value), key, tone: "success" });
|
|
911
|
+
return value;
|
|
912
|
+
} catch (error) {
|
|
913
|
+
notify({ ...resolveMessage(messages.error, error), key, tone: "error" });
|
|
914
|
+
throw error;
|
|
915
|
+
}
|
|
916
|
+
}
|
|
917
|
+
const instance = {
|
|
918
|
+
get items() {
|
|
919
|
+
return Object.freeze([...items]);
|
|
920
|
+
},
|
|
921
|
+
notify,
|
|
922
|
+
update,
|
|
923
|
+
promise,
|
|
924
|
+
dismiss,
|
|
925
|
+
dismissAll() {
|
|
926
|
+
for (const item of items) clearTimer(item.id);
|
|
927
|
+
items = [];
|
|
928
|
+
emit();
|
|
929
|
+
},
|
|
930
|
+
pause() {
|
|
931
|
+
if (paused) return;
|
|
932
|
+
paused = true;
|
|
933
|
+
for (const [id, timer] of timers) {
|
|
934
|
+
if (!timer.handle) continue;
|
|
935
|
+
clearTimeout(timer.handle);
|
|
936
|
+
timers.set(id, {
|
|
937
|
+
handle: null,
|
|
938
|
+
remaining: Math.max(timer.remaining - (Date.now() - timer.startedAt), 0),
|
|
939
|
+
startedAt: Date.now()
|
|
940
|
+
});
|
|
941
|
+
}
|
|
942
|
+
},
|
|
943
|
+
resume() {
|
|
944
|
+
if (!paused) return;
|
|
945
|
+
paused = false;
|
|
946
|
+
for (const [id, timer] of [...timers]) {
|
|
947
|
+
clearTimer(id);
|
|
948
|
+
startTimer(id, timer.remaining);
|
|
949
|
+
}
|
|
950
|
+
},
|
|
951
|
+
subscribe(listener) {
|
|
952
|
+
listeners.add(listener);
|
|
953
|
+
return () => {
|
|
954
|
+
listeners.delete(listener);
|
|
955
|
+
};
|
|
956
|
+
},
|
|
957
|
+
destroy() {
|
|
958
|
+
for (const item of items) clearTimer(item.id);
|
|
959
|
+
items = [];
|
|
960
|
+
listeners.clear();
|
|
961
|
+
view?.removeEventListener("visibilitychange", onVisibility);
|
|
962
|
+
}
|
|
963
|
+
};
|
|
964
|
+
view?.addEventListener("visibilitychange", onVisibility);
|
|
965
|
+
return instance;
|
|
966
|
+
}
|
|
967
|
+
|
|
968
|
+
// src/core/network.ts
|
|
969
|
+
var SERVER_NETWORK_STATE = {
|
|
970
|
+
online: true,
|
|
971
|
+
downlink: null,
|
|
972
|
+
downlinkMax: null,
|
|
973
|
+
effectiveType: null,
|
|
974
|
+
rtt: null,
|
|
975
|
+
saveData: null,
|
|
976
|
+
type: null,
|
|
977
|
+
recovered: false,
|
|
978
|
+
slow: false
|
|
979
|
+
};
|
|
980
|
+
function connection() {
|
|
981
|
+
if (typeof navigator === "undefined") return null;
|
|
982
|
+
const nav = navigator;
|
|
983
|
+
return nav.connection ?? nav.mozConnection ?? nav.webkitConnection ?? null;
|
|
984
|
+
}
|
|
985
|
+
function shallowEqual(left, right) {
|
|
986
|
+
for (const key of Object.keys(left)) {
|
|
987
|
+
if (left[key] !== right[key]) return false;
|
|
988
|
+
}
|
|
989
|
+
return true;
|
|
990
|
+
}
|
|
991
|
+
function createNetworkMonitor() {
|
|
992
|
+
if (typeof window === "undefined") {
|
|
993
|
+
return {
|
|
994
|
+
get state() {
|
|
995
|
+
return SERVER_NETWORK_STATE;
|
|
996
|
+
},
|
|
997
|
+
subscribe() {
|
|
998
|
+
return () => {
|
|
999
|
+
};
|
|
1000
|
+
},
|
|
1001
|
+
destroy() {
|
|
1002
|
+
}
|
|
1003
|
+
};
|
|
1004
|
+
}
|
|
1005
|
+
const listeners = /* @__PURE__ */ new Set();
|
|
1006
|
+
let hasBeenOffline = false;
|
|
1007
|
+
let state = read();
|
|
1008
|
+
function read() {
|
|
1009
|
+
const link2 = connection();
|
|
1010
|
+
const online = navigator.onLine;
|
|
1011
|
+
const effectiveType = link2?.effectiveType ?? null;
|
|
1012
|
+
const saveData = link2?.saveData ?? null;
|
|
1013
|
+
return {
|
|
1014
|
+
online,
|
|
1015
|
+
downlink: link2?.downlink ?? null,
|
|
1016
|
+
downlinkMax: link2?.downlinkMax ?? null,
|
|
1017
|
+
effectiveType,
|
|
1018
|
+
rtt: link2?.rtt ?? null,
|
|
1019
|
+
saveData,
|
|
1020
|
+
type: link2?.type ?? null,
|
|
1021
|
+
recovered: online && hasBeenOffline,
|
|
1022
|
+
slow: saveData === true || effectiveType === "2g" || effectiveType === "slow-2g"
|
|
1023
|
+
};
|
|
1024
|
+
}
|
|
1025
|
+
function update() {
|
|
1026
|
+
if (!navigator.onLine) hasBeenOffline = true;
|
|
1027
|
+
const next = read();
|
|
1028
|
+
if (shallowEqual(state, next)) return;
|
|
1029
|
+
state = next;
|
|
1030
|
+
for (const listener of listeners) listener(state);
|
|
1031
|
+
}
|
|
1032
|
+
const link = connection();
|
|
1033
|
+
window.addEventListener("online", update, { passive: true });
|
|
1034
|
+
window.addEventListener("offline", update, { passive: true });
|
|
1035
|
+
link?.addEventListener("change", update, { passive: true });
|
|
1036
|
+
return {
|
|
1037
|
+
get state() {
|
|
1038
|
+
return state;
|
|
1039
|
+
},
|
|
1040
|
+
subscribe(listener) {
|
|
1041
|
+
listeners.add(listener);
|
|
1042
|
+
return () => {
|
|
1043
|
+
listeners.delete(listener);
|
|
1044
|
+
};
|
|
1045
|
+
},
|
|
1046
|
+
destroy() {
|
|
1047
|
+
listeners.clear();
|
|
1048
|
+
window.removeEventListener("online", update);
|
|
1049
|
+
window.removeEventListener("offline", update);
|
|
1050
|
+
link?.removeEventListener("change", update);
|
|
1051
|
+
}
|
|
1052
|
+
};
|
|
1053
|
+
}
|
|
1054
|
+
|
|
1055
|
+
// src/core/relative-time.ts
|
|
1056
|
+
var ZONED = /([zZ]|[+-]\d{2}:?\d{2})$/;
|
|
1057
|
+
var DATE_ONLY = /^\d{4}-\d{2}-\d{2}$/;
|
|
1058
|
+
var HAS_TIME = /\d{2}:\d{2}/;
|
|
1059
|
+
var DURATION = /^([+-])?P(?:(\d+(?:\.\d+)?)Y)?(?:(\d+(?:\.\d+)?)M)?(?:(\d+(?:\.\d+)?)W)?(?:(\d+(?:\.\d+)?)D)?(?:T(?:(\d+(?:\.\d+)?)H)?(?:(\d+(?:\.\d+)?)M)?(?:(\d+(?:\.\d+)?)S)?)?$/;
|
|
1060
|
+
var SECOND = 1e3;
|
|
1061
|
+
var MINUTE = 60 * SECOND;
|
|
1062
|
+
var HOUR = 60 * MINUTE;
|
|
1063
|
+
var DAY = 24 * HOUR;
|
|
1064
|
+
var WEEK = 7 * DAY;
|
|
1065
|
+
var MONTH = 30 * DAY;
|
|
1066
|
+
var YEAR = 365 * DAY;
|
|
1067
|
+
function ensureZone(text) {
|
|
1068
|
+
const trimmed = text.trim();
|
|
1069
|
+
if (!trimmed || DATE_ONLY.test(trimmed) || ZONED.test(trimmed)) return trimmed;
|
|
1070
|
+
const iso = trimmed.replace(" ", "T");
|
|
1071
|
+
return HAS_TIME.test(iso) ? `${iso}Z` : iso;
|
|
1072
|
+
}
|
|
1073
|
+
function normalizeDate(value) {
|
|
1074
|
+
if (value == null) return null;
|
|
1075
|
+
if (value instanceof Date) return Number.isNaN(value.getTime()) ? null : value;
|
|
1076
|
+
if (typeof value === "number") return Number.isFinite(value) ? new Date(value) : null;
|
|
1077
|
+
const date = new Date(ensureZone(value));
|
|
1078
|
+
return Number.isNaN(date.getTime()) ? null : date;
|
|
1079
|
+
}
|
|
1080
|
+
function parseDuration(value) {
|
|
1081
|
+
const match = DURATION.exec(value.trim());
|
|
1082
|
+
if (!match) return 0;
|
|
1083
|
+
const [, sign, years, months, weeks, days, hours, minutes, seconds] = match;
|
|
1084
|
+
const n = (part) => part ? Number(part) : 0;
|
|
1085
|
+
const total = n(years) * YEAR + n(months) * MONTH + n(weeks) * WEEK + n(days) * DAY + n(hours) * HOUR + n(minutes) * MINUTE + n(seconds) * SECOND;
|
|
1086
|
+
return sign === "-" ? -total : total;
|
|
1087
|
+
}
|
|
1088
|
+
function dateTimeOptions(options) {
|
|
1089
|
+
const { second, minute, hour, weekday, day, month, year, timeZone, timeZoneName } = options;
|
|
1090
|
+
const parts = { timeZone, timeZoneName, second, minute, hour, weekday, day, month, year };
|
|
1091
|
+
for (const key of Object.keys(parts)) {
|
|
1092
|
+
if (parts[key] === void 0) delete parts[key];
|
|
1093
|
+
}
|
|
1094
|
+
return parts;
|
|
1095
|
+
}
|
|
1096
|
+
function format(date, locale, options) {
|
|
1097
|
+
try {
|
|
1098
|
+
return new Intl.DateTimeFormat(locale, options).format(date);
|
|
1099
|
+
} catch {
|
|
1100
|
+
return date.toISOString();
|
|
1101
|
+
}
|
|
1102
|
+
}
|
|
1103
|
+
function relativeTimeView(value, options = {}, now = /* @__PURE__ */ new Date()) {
|
|
1104
|
+
const date = normalizeDate(value);
|
|
1105
|
+
if (!date) return { date: null, iso: "", label: "", exact: "", beyondThreshold: false, absoluteOnly: false };
|
|
1106
|
+
const { locale, threshold = "P30D", prefix = "on", numericBeyondThreshold = false } = options;
|
|
1107
|
+
const age = Math.abs(date.getTime() - now.getTime());
|
|
1108
|
+
const beyondThreshold = age > parseDuration(threshold);
|
|
1109
|
+
const absoluteOnly = numericBeyondThreshold && beyondThreshold;
|
|
1110
|
+
const parts = absoluteOnly ? { day: "numeric", month: "numeric", year: "numeric", timeZone: options.timeZone } : { day: "numeric", month: "short", year: "numeric", ...dateTimeOptions(options) };
|
|
1111
|
+
const label = format(date, locale, parts);
|
|
1112
|
+
return {
|
|
1113
|
+
date,
|
|
1114
|
+
iso: date.toISOString(),
|
|
1115
|
+
label: !absoluteOnly && prefix ? `${prefix} ${label}` : label,
|
|
1116
|
+
exact: format(date, locale, { dateStyle: "full", timeStyle: "long", timeZone: options.timeZone }),
|
|
1117
|
+
beyondThreshold,
|
|
1118
|
+
absoluteOnly
|
|
1119
|
+
};
|
|
1120
|
+
}
|
|
1121
|
+
function relativeTimeAttributes(view, options = {}) {
|
|
1122
|
+
const attributes = {
|
|
1123
|
+
datetime: view.iso,
|
|
1124
|
+
format: options.format,
|
|
1125
|
+
tense: options.tense,
|
|
1126
|
+
precision: options.precision,
|
|
1127
|
+
threshold: options.threshold,
|
|
1128
|
+
prefix: options.prefix,
|
|
1129
|
+
"format-style": options.formatStyle,
|
|
1130
|
+
second: options.second,
|
|
1131
|
+
minute: options.minute,
|
|
1132
|
+
hour: options.hour,
|
|
1133
|
+
weekday: options.weekday,
|
|
1134
|
+
day: options.day,
|
|
1135
|
+
month: options.month,
|
|
1136
|
+
year: options.year,
|
|
1137
|
+
lang: options.locale,
|
|
1138
|
+
"time-zone": options.timeZone,
|
|
1139
|
+
"time-zone-name": options.timeZoneName,
|
|
1140
|
+
// Boolean attributes are read by presence: "false" would still be true.
|
|
1141
|
+
"no-title": options.noTitle ? "" : void 0
|
|
1142
|
+
};
|
|
1143
|
+
const out = {};
|
|
1144
|
+
for (const [key, value] of Object.entries(attributes)) {
|
|
1145
|
+
if (value !== void 0) out[key] = value;
|
|
1146
|
+
}
|
|
1147
|
+
return out;
|
|
1148
|
+
}
|
|
1149
|
+
|
|
1150
|
+
export { INPUT_ICON_PATHS, SERVER_NETWORK_STATE, createButton, createInput, createMarquee, createNetworkMonitor, createNotifications, ensureZone, estimatePasswordStrength, generatePassword, iconMarkup, normalizeDate, parseDuration, relativeTimeAttributes, relativeTimeView };
|
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import { generatePassword, estimatePasswordStrength, INPUT_ICON_PATHS, createButton, createMarquee, createInput } from './chunk-
|
|
1
|
+
import { generatePassword, estimatePasswordStrength, INPUT_ICON_PATHS, createNotifications, createButton, createMarquee, createInput, createNetworkMonitor, SERVER_NETWORK_STATE, relativeTimeView, relativeTimeAttributes } from './chunk-DD2X5B2J.js';
|
|
2
2
|
import { createSearch } from './chunk-UZFEEFMF.js';
|
|
3
|
-
import { forwardRef, createElement, useRef, useState, useId, useCallback, useEffect, useMemo, useLayoutEffect } from 'react';
|
|
4
|
-
import { jsx, jsxs } from 'react/jsx-runtime';
|
|
3
|
+
import { forwardRef, createElement, useRef, useState, useId, useCallback, useEffect, useMemo, useSyncExternalStore, useLayoutEffect } from 'react';
|
|
4
|
+
import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
|
|
5
5
|
|
|
6
6
|
var useIsomorphicLayoutEffect = typeof window === "undefined" ? useEffect : useLayoutEffect;
|
|
7
7
|
function useMarquee(options = {}) {
|
|
@@ -198,7 +198,7 @@ var Button = forwardRef(function Button2({
|
|
|
198
198
|
pending,
|
|
199
199
|
href,
|
|
200
200
|
disabled,
|
|
201
|
-
loading,
|
|
201
|
+
loading: loading2,
|
|
202
202
|
cooldown,
|
|
203
203
|
shortcut,
|
|
204
204
|
onPress,
|
|
@@ -207,7 +207,7 @@ var Button = forwardRef(function Button2({
|
|
|
207
207
|
type = "button",
|
|
208
208
|
...rest
|
|
209
209
|
}, ref) {
|
|
210
|
-
const button = useButton({ href, disabled, loading, cooldown, shortcut, onPress: onPress ?? onClick, onChange });
|
|
210
|
+
const button = useButton({ href, disabled, loading: loading2, cooldown, shortcut, onPress: onPress ?? onClick, onChange });
|
|
211
211
|
const state = button;
|
|
212
212
|
const Tag = as ?? (state.element === "a" ? getLinkComponent() : "button");
|
|
213
213
|
const native = Tag === "button" ? { type } : {};
|
|
@@ -463,5 +463,256 @@ function PasswordStrength({
|
|
|
463
463
|
}
|
|
464
464
|
);
|
|
465
465
|
}
|
|
466
|
+
var defaultQueue = createNotifications();
|
|
467
|
+
function useNotifications(queue = defaultQueue) {
|
|
468
|
+
const snapshot = useRef(queue.items);
|
|
469
|
+
const items = useSyncExternalStore(
|
|
470
|
+
(listener) => queue.subscribe((next) => {
|
|
471
|
+
snapshot.current = next;
|
|
472
|
+
listener();
|
|
473
|
+
}),
|
|
474
|
+
() => snapshot.current,
|
|
475
|
+
() => snapshot.current
|
|
476
|
+
);
|
|
477
|
+
return useMemo(() => ({
|
|
478
|
+
items,
|
|
479
|
+
notify: queue.notify,
|
|
480
|
+
dismiss: queue.dismiss,
|
|
481
|
+
dismissAll: queue.dismissAll,
|
|
482
|
+
pause: queue.pause,
|
|
483
|
+
resume: queue.resume
|
|
484
|
+
}), [items, queue]);
|
|
485
|
+
}
|
|
486
|
+
function createNotificationQueue(options) {
|
|
487
|
+
return createNotifications(options);
|
|
488
|
+
}
|
|
489
|
+
function swipeAxis(position) {
|
|
490
|
+
if (position.endsWith("-center")) return { axis: "y", sign: position.startsWith("top") ? -1 : 1 };
|
|
491
|
+
return { axis: "x", sign: position.endsWith("-left") ? -1 : 1 };
|
|
492
|
+
}
|
|
493
|
+
function Toaster({
|
|
494
|
+
queue = defaultQueue,
|
|
495
|
+
position = "bottom-right",
|
|
496
|
+
exitDuration = 200,
|
|
497
|
+
swipeThreshold = 60,
|
|
498
|
+
children,
|
|
499
|
+
label = "Notifications",
|
|
500
|
+
className,
|
|
501
|
+
style
|
|
502
|
+
}) {
|
|
503
|
+
const [rendered, setRendered] = useState(
|
|
504
|
+
() => queue.items.map((notification) => ({ notification, leaving: false }))
|
|
505
|
+
);
|
|
506
|
+
const timers = useRef(/* @__PURE__ */ new Map());
|
|
507
|
+
useEffect(() => {
|
|
508
|
+
const sync = (items) => {
|
|
509
|
+
setRendered((current) => {
|
|
510
|
+
const live = new Map(items.map((item) => [item.id, item]));
|
|
511
|
+
const kept = current.map((entry) => {
|
|
512
|
+
const still = live.get(entry.notification.id);
|
|
513
|
+
if (still) return { notification: still, leaving: false };
|
|
514
|
+
if (entry.leaving) return entry;
|
|
515
|
+
const timer = setTimeout(() => {
|
|
516
|
+
timers.current.delete(entry.notification.id);
|
|
517
|
+
setRendered((later) => later.filter((row) => row.notification.id !== entry.notification.id));
|
|
518
|
+
}, exitDuration);
|
|
519
|
+
timers.current.set(entry.notification.id, timer);
|
|
520
|
+
return { ...entry, leaving: true };
|
|
521
|
+
});
|
|
522
|
+
const seen = new Set(kept.map((entry) => entry.notification.id));
|
|
523
|
+
const added = items.filter((item) => !seen.has(item.id)).map((item) => ({ notification: item, leaving: false }));
|
|
524
|
+
return [...kept, ...added];
|
|
525
|
+
});
|
|
526
|
+
};
|
|
527
|
+
sync(queue.items);
|
|
528
|
+
return queue.subscribe(sync);
|
|
529
|
+
}, [queue, exitDuration]);
|
|
530
|
+
useEffect(() => {
|
|
531
|
+
const pending = timers.current;
|
|
532
|
+
return () => {
|
|
533
|
+
for (const timer of pending.values()) clearTimeout(timer);
|
|
534
|
+
pending.clear();
|
|
535
|
+
};
|
|
536
|
+
}, []);
|
|
537
|
+
if (!rendered.length) return null;
|
|
538
|
+
return /* @__PURE__ */ jsx(
|
|
539
|
+
"section",
|
|
540
|
+
{
|
|
541
|
+
"aria-label": label,
|
|
542
|
+
"data-enigma-toaster": "",
|
|
543
|
+
"data-position": position,
|
|
544
|
+
className,
|
|
545
|
+
style,
|
|
546
|
+
onPointerEnter: () => queue.pause(),
|
|
547
|
+
onPointerLeave: () => queue.resume(),
|
|
548
|
+
onFocusCapture: () => queue.pause(),
|
|
549
|
+
onBlurCapture: () => queue.resume(),
|
|
550
|
+
children: rendered.map((entry, index) => /* @__PURE__ */ jsx(
|
|
551
|
+
Toast,
|
|
552
|
+
{
|
|
553
|
+
entry,
|
|
554
|
+
index: rendered.length - 1 - index,
|
|
555
|
+
position,
|
|
556
|
+
swipeThreshold,
|
|
557
|
+
onDismiss: () => queue.dismiss(entry.notification.id),
|
|
558
|
+
render: children
|
|
559
|
+
},
|
|
560
|
+
entry.notification.id
|
|
561
|
+
))
|
|
562
|
+
}
|
|
563
|
+
);
|
|
564
|
+
}
|
|
565
|
+
function Toast({ entry, index, position, swipeThreshold, onDismiss, render }) {
|
|
566
|
+
const { notification, leaving } = entry;
|
|
567
|
+
const [offset, setOffset] = useState(0);
|
|
568
|
+
const [swiping, setSwiping] = useState(false);
|
|
569
|
+
const start = useRef(0);
|
|
570
|
+
const { axis, sign } = swipeAxis(position);
|
|
571
|
+
const onPointerDown = useCallback((event) => {
|
|
572
|
+
if (event.target.closest("button")) return;
|
|
573
|
+
if (event.pointerType === "mouse" && event.button !== 0) return;
|
|
574
|
+
event.currentTarget.setPointerCapture(event.pointerId);
|
|
575
|
+
start.current = axis === "x" ? event.clientX : event.clientY;
|
|
576
|
+
setSwiping(true);
|
|
577
|
+
}, [axis]);
|
|
578
|
+
const onPointerMove = useCallback((event) => {
|
|
579
|
+
if (!swiping) return;
|
|
580
|
+
const delta = (axis === "x" ? event.clientX : event.clientY) - start.current;
|
|
581
|
+
setOffset(sign > 0 ? Math.max(0, delta) : Math.min(0, delta));
|
|
582
|
+
}, [swiping, axis, sign]);
|
|
583
|
+
const onPointerUp = useCallback(() => {
|
|
584
|
+
if (!swiping) return;
|
|
585
|
+
setSwiping(false);
|
|
586
|
+
if (Math.abs(offset) >= swipeThreshold) {
|
|
587
|
+
onDismiss();
|
|
588
|
+
return;
|
|
589
|
+
}
|
|
590
|
+
setOffset(0);
|
|
591
|
+
}, [swiping, offset, swipeThreshold, onDismiss]);
|
|
592
|
+
const controls = { dismiss: onDismiss };
|
|
593
|
+
return /* @__PURE__ */ jsx(
|
|
594
|
+
"article",
|
|
595
|
+
{
|
|
596
|
+
"data-enigma-toast": "",
|
|
597
|
+
"data-tone": notification.tone,
|
|
598
|
+
"data-state": leaving ? "leaving" : "open",
|
|
599
|
+
"data-index": index,
|
|
600
|
+
"data-front": index === 0 ? "" : void 0,
|
|
601
|
+
"data-swiping": swiping ? "" : void 0,
|
|
602
|
+
role: notification.tone === "error" ? "alert" : "status",
|
|
603
|
+
"aria-live": notification.tone === "error" ? "assertive" : "polite",
|
|
604
|
+
tabIndex: 0,
|
|
605
|
+
onKeyDown: (event) => {
|
|
606
|
+
if (event.key === "Escape") onDismiss();
|
|
607
|
+
},
|
|
608
|
+
onPointerDown,
|
|
609
|
+
onPointerMove,
|
|
610
|
+
onPointerUp,
|
|
611
|
+
onPointerCancel: onPointerUp,
|
|
612
|
+
style: {
|
|
613
|
+
// Read by the theme, so the transform stays the stylesheet's decision.
|
|
614
|
+
"--enigma-toast-swipe": `${offset}px`,
|
|
615
|
+
"--enigma-toast-index": index
|
|
616
|
+
},
|
|
617
|
+
children: render ? render(notification, controls) : /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
618
|
+
/* @__PURE__ */ jsx("p", { "data-enigma-toast-title": "", children: notification.title }),
|
|
619
|
+
notification.body && /* @__PURE__ */ jsx("p", { "data-enigma-toast-body": "", children: notification.body }),
|
|
620
|
+
notification.action && /* @__PURE__ */ jsx(
|
|
621
|
+
"button",
|
|
622
|
+
{
|
|
623
|
+
type: "button",
|
|
624
|
+
"data-enigma-toast-action": "",
|
|
625
|
+
onClick: () => {
|
|
626
|
+
notification.action?.onSelect();
|
|
627
|
+
if (notification.action?.dismiss !== false) onDismiss();
|
|
628
|
+
},
|
|
629
|
+
children: notification.action.label
|
|
630
|
+
}
|
|
631
|
+
),
|
|
632
|
+
/* @__PURE__ */ jsx("button", { type: "button", "data-enigma-toast-close": "", "aria-label": "Dismiss", onClick: onDismiss, children: "\xD7" })
|
|
633
|
+
] })
|
|
634
|
+
}
|
|
635
|
+
);
|
|
636
|
+
}
|
|
637
|
+
function useNetworkState() {
|
|
638
|
+
const monitor = useMemo(() => createNetworkMonitor(), []);
|
|
639
|
+
const snapshot = useRef(monitor.state);
|
|
640
|
+
return useSyncExternalStore(
|
|
641
|
+
(listener) => monitor.subscribe((next) => {
|
|
642
|
+
snapshot.current = next;
|
|
643
|
+
listener();
|
|
644
|
+
}),
|
|
645
|
+
() => snapshot.current,
|
|
646
|
+
() => SERVER_NETWORK_STATE
|
|
647
|
+
);
|
|
648
|
+
}
|
|
649
|
+
var loading = null;
|
|
650
|
+
function loadElement() {
|
|
651
|
+
if (loading || typeof window === "undefined") return;
|
|
652
|
+
loading = import('@github/relative-time-element').catch(() => {
|
|
653
|
+
return null;
|
|
654
|
+
});
|
|
655
|
+
}
|
|
656
|
+
function RelativeTime({
|
|
657
|
+
date,
|
|
658
|
+
fallback = null,
|
|
659
|
+
capitalize = false,
|
|
660
|
+
capitalizeFirst = true,
|
|
661
|
+
now,
|
|
662
|
+
format,
|
|
663
|
+
tense,
|
|
664
|
+
precision,
|
|
665
|
+
threshold,
|
|
666
|
+
prefix,
|
|
667
|
+
formatStyle,
|
|
668
|
+
locale,
|
|
669
|
+
timeZone,
|
|
670
|
+
second,
|
|
671
|
+
minute,
|
|
672
|
+
hour,
|
|
673
|
+
weekday,
|
|
674
|
+
day,
|
|
675
|
+
month,
|
|
676
|
+
year,
|
|
677
|
+
timeZoneName,
|
|
678
|
+
noTitle,
|
|
679
|
+
numericBeyondThreshold,
|
|
680
|
+
...rest
|
|
681
|
+
}) {
|
|
682
|
+
useEffect(loadElement, []);
|
|
683
|
+
const options = {
|
|
684
|
+
format,
|
|
685
|
+
tense,
|
|
686
|
+
precision,
|
|
687
|
+
threshold,
|
|
688
|
+
prefix,
|
|
689
|
+
formatStyle,
|
|
690
|
+
locale,
|
|
691
|
+
timeZone,
|
|
692
|
+
second,
|
|
693
|
+
minute,
|
|
694
|
+
hour,
|
|
695
|
+
weekday,
|
|
696
|
+
day,
|
|
697
|
+
month,
|
|
698
|
+
year,
|
|
699
|
+
timeZoneName,
|
|
700
|
+
noTitle,
|
|
701
|
+
numericBeyondThreshold
|
|
702
|
+
};
|
|
703
|
+
const view = relativeTimeView(date, options, now);
|
|
704
|
+
if (!view.date) return fallback;
|
|
705
|
+
const marks = {
|
|
706
|
+
"data-relative-time-capitalize": capitalize ? "true" : void 0,
|
|
707
|
+
"data-relative-time-capitalize-first-letter": capitalizeFirst ? "true" : void 0,
|
|
708
|
+
// The text differs between the server's instant and the browser's, which is the
|
|
709
|
+
// one hydration difference React has an escape hatch for rather than a bug.
|
|
710
|
+
suppressHydrationWarning: true
|
|
711
|
+
};
|
|
712
|
+
if (view.absoluteOnly) {
|
|
713
|
+
return createElement("time", { dateTime: view.iso, title: noTitle ? void 0 : view.exact, ...marks, ...rest }, view.label);
|
|
714
|
+
}
|
|
715
|
+
return createElement("relative-time", { ...relativeTimeAttributes(view, options), ...marks, ...rest }, view.label);
|
|
716
|
+
}
|
|
466
717
|
|
|
467
|
-
export { Button, Input, PasswordStrength, getLinkComponent, setLinkComponent, useButton, useInput, useMarquee, useSearch };
|
|
718
|
+
export { Button, Input, PasswordStrength, RelativeTime, Toaster, createNotificationQueue, defaultQueue, getLinkComponent, setLinkComponent, useButton, useInput, useMarquee, useNetworkState, useNotifications, useSearch };
|