@ttt-productions/ui-core 0.2.32 → 0.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/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/lib/format-numbers.d.ts +8 -0
- package/dist/lib/format-numbers.d.ts.map +1 -0
- package/dist/lib/format-numbers.js +18 -0
- package/dist/lib/format-numbers.js.map +1 -0
- package/dist/react/components/chunk-error-recovery.d.ts +13 -0
- package/dist/react/components/chunk-error-recovery.d.ts.map +1 -0
- package/dist/react/components/chunk-error-recovery.js +35 -0
- package/dist/react/components/chunk-error-recovery.js.map +1 -0
- package/dist/react/components/end-of-list-indicator.d.ts +8 -0
- package/dist/react/components/end-of-list-indicator.d.ts.map +1 -0
- package/dist/react/components/end-of-list-indicator.js +7 -0
- package/dist/react/components/end-of-list-indicator.js.map +1 -0
- package/dist/react/components/relative-time.d.ts +16 -0
- package/dist/react/components/relative-time.d.ts.map +1 -0
- package/dist/react/components/relative-time.js +39 -0
- package/dist/react/components/relative-time.js.map +1 -0
- package/dist/react/components/scroll-to-top-button.d.ts +13 -0
- package/dist/react/components/scroll-to-top-button.d.ts.map +1 -0
- package/dist/react/components/scroll-to-top-button.js +18 -0
- package/dist/react/components/scroll-to-top-button.js.map +1 -0
- package/dist/react/index.d.ts +8 -0
- package/dist/react/index.d.ts.map +1 -1
- package/dist/react/index.js +5 -0
- package/dist/react/index.js.map +1 -1
- package/package.json +5 -3
- package/src/index.ts +1 -0
- package/src/lib/format-numbers.ts +17 -0
- package/src/react/components/chunk-error-recovery.tsx +57 -0
- package/src/react/components/end-of-list-indicator.tsx +27 -0
- package/src/react/components/relative-time.tsx +56 -0
- package/src/react/components/scroll-to-top-button.tsx +50 -0
- package/src/react/index.ts +13 -0
package/dist/index.d.ts
CHANGED
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AACA,cAAc,gBAAgB,CAAC"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AACA,cAAc,gBAAgB,CAAC;AAC/B,cAAc,yBAAyB,CAAC"}
|
package/dist/index.js
CHANGED
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,8EAA8E;AAC9E,cAAc,gBAAgB,CAAC"}
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,8EAA8E;AAC9E,cAAc,gBAAgB,CAAC;AAC/B,cAAc,yBAAyB,CAAC"}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Compact number formatter:
|
|
3
|
+
* < 10,000 → "1234"
|
|
4
|
+
* 10,000 ≤ n < 1,000,000 → "12.3k" (trailing ".0" stripped)
|
|
5
|
+
* ≥ 1,000,000 → "1.2M" (trailing ".0" stripped)
|
|
6
|
+
*/
|
|
7
|
+
export declare function formatLargeNumber(num: number): string;
|
|
8
|
+
//# sourceMappingURL=format-numbers.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"format-numbers.d.ts","sourceRoot":"","sources":["../../src/lib/format-numbers.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AACH,wBAAgB,iBAAiB,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAUrD"}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Compact number formatter:
|
|
3
|
+
* < 10,000 → "1234"
|
|
4
|
+
* 10,000 ≤ n < 1,000,000 → "12.3k" (trailing ".0" stripped)
|
|
5
|
+
* ≥ 1,000,000 → "1.2M" (trailing ".0" stripped)
|
|
6
|
+
*/
|
|
7
|
+
export function formatLargeNumber(num) {
|
|
8
|
+
if (num >= 1_000_000) {
|
|
9
|
+
const formatted = (num / 1_000_000).toFixed(1);
|
|
10
|
+
return (formatted.endsWith(".0") ? formatted.slice(0, -2) : formatted) + "M";
|
|
11
|
+
}
|
|
12
|
+
if (num >= 10_000) {
|
|
13
|
+
const formatted = (num / 1000).toFixed(1);
|
|
14
|
+
return (formatted.endsWith(".0") ? formatted.slice(0, -2) : formatted) + "k";
|
|
15
|
+
}
|
|
16
|
+
return num.toString();
|
|
17
|
+
}
|
|
18
|
+
//# sourceMappingURL=format-numbers.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"format-numbers.js","sourceRoot":"","sources":["../../src/lib/format-numbers.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AACH,MAAM,UAAU,iBAAiB,CAAC,GAAW;IAC3C,IAAI,GAAG,IAAI,SAAS,EAAE,CAAC;QACrB,MAAM,SAAS,GAAG,CAAC,GAAG,GAAG,SAAS,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC;QAC/C,OAAO,CAAC,SAAS,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC,GAAG,GAAG,CAAC;IAC/E,CAAC;IACD,IAAI,GAAG,IAAI,MAAM,EAAE,CAAC;QAClB,MAAM,SAAS,GAAG,CAAC,GAAG,GAAG,IAAI,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC;QAC1C,OAAO,CAAC,SAAS,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC,GAAG,GAAG,CAAC;IAC/E,CAAC;IACD,OAAO,GAAG,CAAC,QAAQ,EAAE,CAAC;AACxB,CAAC"}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
export interface ChunkErrorRecoveryProps {
|
|
3
|
+
children: React.ReactNode;
|
|
4
|
+
/** Custom message shown during recovery reload. */
|
|
5
|
+
loadingMessage?: React.ReactNode;
|
|
6
|
+
/**
|
|
7
|
+
* Predicate to decide whether an error event indicates a stale-chunk failure.
|
|
8
|
+
* Defaults to matching "Failed to load chunk" in the message.
|
|
9
|
+
*/
|
|
10
|
+
isChunkError?: (event: ErrorEvent) => boolean;
|
|
11
|
+
}
|
|
12
|
+
export declare function ChunkErrorRecovery({ children, loadingMessage, isChunkError, }: ChunkErrorRecoveryProps): import("react/jsx-runtime").JSX.Element;
|
|
13
|
+
//# sourceMappingURL=chunk-error-recovery.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"chunk-error-recovery.d.ts","sourceRoot":"","sources":["../../../src/react/components/chunk-error-recovery.tsx"],"names":[],"mappings":"AAEA,OAAO,KAAK,KAAK,MAAM,OAAO,CAAC;AAG/B,MAAM,WAAW,uBAAuB;IACtC,QAAQ,EAAE,KAAK,CAAC,SAAS,CAAC;IAC1B,mDAAmD;IACnD,cAAc,CAAC,EAAE,KAAK,CAAC,SAAS,CAAC;IACjC;;;OAGG;IACH,YAAY,CAAC,EAAE,CAAC,KAAK,EAAE,UAAU,KAAK,OAAO,CAAC;CAC/C;AAYD,wBAAgB,kBAAkB,CAAC,EACjC,QAAQ,EACR,cAA4B,EAC5B,YAAkC,GACnC,EAAE,uBAAuB,2CA0BzB"}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
3
|
+
import * as React from "react";
|
|
4
|
+
import { Loader2 } from "lucide-react";
|
|
5
|
+
function defaultIsChunkError(event) {
|
|
6
|
+
const msg = event.message ?? "";
|
|
7
|
+
if (msg.includes("Failed to load chunk"))
|
|
8
|
+
return true;
|
|
9
|
+
const nested = event.error && typeof event.error === "object"
|
|
10
|
+
? event.error.message
|
|
11
|
+
: undefined;
|
|
12
|
+
return typeof nested === "string" && nested.includes("Failed to load chunk");
|
|
13
|
+
}
|
|
14
|
+
export function ChunkErrorRecovery({ children, loadingMessage = "Updating…", isChunkError = defaultIsChunkError, }) {
|
|
15
|
+
const [hasError, setHasError] = React.useState(false);
|
|
16
|
+
React.useEffect(() => {
|
|
17
|
+
const handler = (event) => {
|
|
18
|
+
if (!isChunkError(event))
|
|
19
|
+
return;
|
|
20
|
+
// eslint-disable-next-line no-console
|
|
21
|
+
console.error("Stale chunk detected — reloading.");
|
|
22
|
+
setHasError(true);
|
|
23
|
+
window.location.reload();
|
|
24
|
+
event.preventDefault();
|
|
25
|
+
event.stopPropagation();
|
|
26
|
+
};
|
|
27
|
+
window.addEventListener("error", handler);
|
|
28
|
+
return () => window.removeEventListener("error", handler);
|
|
29
|
+
}, [isChunkError]);
|
|
30
|
+
if (hasError) {
|
|
31
|
+
return (_jsxs("div", { className: "flex flex-col h-screen w-screen items-center justify-center gap-4 bg-background", children: [_jsx(Loader2, { className: "h-10 w-10 animate-spin" }), _jsx("p", { className: "text-muted-foreground", children: loadingMessage })] }));
|
|
32
|
+
}
|
|
33
|
+
return _jsx(_Fragment, { children: children });
|
|
34
|
+
}
|
|
35
|
+
//# sourceMappingURL=chunk-error-recovery.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"chunk-error-recovery.js","sourceRoot":"","sources":["../../../src/react/components/chunk-error-recovery.tsx"],"names":[],"mappings":"AAAA,YAAY,CAAC;;AAEb,OAAO,KAAK,KAAK,MAAM,OAAO,CAAC;AAC/B,OAAO,EAAE,OAAO,EAAE,MAAM,cAAc,CAAC;AAavC,SAAS,mBAAmB,CAAC,KAAiB;IAC5C,MAAM,GAAG,GAAG,KAAK,CAAC,OAAO,IAAI,EAAE,CAAC;IAChC,IAAI,GAAG,CAAC,QAAQ,CAAC,sBAAsB,CAAC;QAAE,OAAO,IAAI,CAAC;IACtD,MAAM,MAAM,GACV,KAAK,CAAC,KAAK,IAAI,OAAO,KAAK,CAAC,KAAK,KAAK,QAAQ;QAC5C,CAAC,CAAE,KAAK,CAAC,KAA8B,CAAC,OAAO;QAC/C,CAAC,CAAC,SAAS,CAAC;IAChB,OAAO,OAAO,MAAM,KAAK,QAAQ,IAAI,MAAM,CAAC,QAAQ,CAAC,sBAAsB,CAAC,CAAC;AAC/E,CAAC;AAED,MAAM,UAAU,kBAAkB,CAAC,EACjC,QAAQ,EACR,cAAc,GAAG,WAAW,EAC5B,YAAY,GAAG,mBAAmB,GACV;IACxB,MAAM,CAAC,QAAQ,EAAE,WAAW,CAAC,GAAG,KAAK,CAAC,QAAQ,CAAC,KAAK,CAAC,CAAC;IAEtD,KAAK,CAAC,SAAS,CAAC,GAAG,EAAE;QACnB,MAAM,OAAO,GAAG,CAAC,KAAiB,EAAE,EAAE;YACpC,IAAI,CAAC,YAAY,CAAC,KAAK,CAAC;gBAAE,OAAO;YACjC,sCAAsC;YACtC,OAAO,CAAC,KAAK,CAAC,mCAAmC,CAAC,CAAC;YACnD,WAAW,CAAC,IAAI,CAAC,CAAC;YAClB,MAAM,CAAC,QAAQ,CAAC,MAAM,EAAE,CAAC;YACzB,KAAK,CAAC,cAAc,EAAE,CAAC;YACvB,KAAK,CAAC,eAAe,EAAE,CAAC;QAC1B,CAAC,CAAC;QACF,MAAM,CAAC,gBAAgB,CAAC,OAAO,EAAE,OAAO,CAAC,CAAC;QAC1C,OAAO,GAAG,EAAE,CAAC,MAAM,CAAC,mBAAmB,CAAC,OAAO,EAAE,OAAO,CAAC,CAAC;IAC5D,CAAC,EAAE,CAAC,YAAY,CAAC,CAAC,CAAC;IAEnB,IAAI,QAAQ,EAAE,CAAC;QACb,OAAO,CACL,eAAK,SAAS,EAAC,iFAAiF,aAC9F,KAAC,OAAO,IAAC,SAAS,EAAC,wBAAwB,GAAG,EAC9C,YAAG,SAAS,EAAC,uBAAuB,YAAE,cAAc,GAAK,IACrD,CACP,CAAC;IACJ,CAAC;IACD,OAAO,4BAAG,QAAQ,GAAI,CAAC;AACzB,CAAC"}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
export interface EndOfListIndicatorProps {
|
|
3
|
+
message?: string;
|
|
4
|
+
className?: string;
|
|
5
|
+
icon?: React.ReactNode;
|
|
6
|
+
}
|
|
7
|
+
export declare function EndOfListIndicator({ message, className, icon, }: EndOfListIndicatorProps): import("react/jsx-runtime").JSX.Element;
|
|
8
|
+
//# sourceMappingURL=end-of-list-indicator.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"end-of-list-indicator.d.ts","sourceRoot":"","sources":["../../../src/react/components/end-of-list-indicator.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,KAAK,MAAM,OAAO,CAAC;AAI/B,MAAM,WAAW,uBAAuB;IACtC,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,IAAI,CAAC,EAAE,KAAK,CAAC,SAAS,CAAC;CACxB;AAED,wBAAgB,kBAAkB,CAAC,EACjC,OAAmC,EACnC,SAAS,EACT,IAAI,GACL,EAAE,uBAAuB,2CAYzB"}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { CheckCircle2 } from "lucide-react";
|
|
3
|
+
import { cn } from "../../lib/utils.js";
|
|
4
|
+
export function EndOfListIndicator({ message = "You've reached the end!", className, icon, }) {
|
|
5
|
+
return (_jsxs("div", { className: cn("flex flex-col items-center gap-2 py-8 text-center text-muted-foreground", className), children: [icon ?? _jsx(CheckCircle2, { className: "h-6 w-6 text-emerald-500" }), _jsx("p", { className: "text-sm", children: message })] }));
|
|
6
|
+
}
|
|
7
|
+
//# sourceMappingURL=end-of-list-indicator.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"end-of-list-indicator.js","sourceRoot":"","sources":["../../../src/react/components/end-of-list-indicator.tsx"],"names":[],"mappings":";AACA,OAAO,EAAE,YAAY,EAAE,MAAM,cAAc,CAAC;AAC5C,OAAO,EAAE,EAAE,EAAE,MAAM,oBAAoB,CAAC;AAQxC,MAAM,UAAU,kBAAkB,CAAC,EACjC,OAAO,GAAG,yBAAyB,EACnC,SAAS,EACT,IAAI,GACoB;IACxB,OAAO,CACL,eACE,SAAS,EAAE,EAAE,CACX,yEAAyE,EACzE,SAAS,CACV,aAEA,IAAI,IAAI,KAAC,YAAY,IAAC,SAAS,EAAC,0BAA0B,GAAG,EAC9D,YAAG,SAAS,EAAC,SAAS,YAAE,OAAO,GAAK,IAChC,CACP,CAAC;AACJ,CAAC"}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export interface RelativeTimeProps {
|
|
2
|
+
/** Number (ms or seconds), ISO string, Firestore Timestamp, or Date. */
|
|
3
|
+
timestamp: number | string | Date | undefined | null;
|
|
4
|
+
/** Fallback rendered before client hydration completes. Defaults to "...". */
|
|
5
|
+
placeholder?: string;
|
|
6
|
+
/** Fallback rendered when the timestamp can't be parsed. Defaults to "a while ago". */
|
|
7
|
+
fallback?: string;
|
|
8
|
+
/** Auto-refresh interval in ms. Defaults to 60000. Set to 0 to disable. */
|
|
9
|
+
refreshIntervalMs?: number;
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* SSR-safe relative-time component.
|
|
13
|
+
* Renders `placeholder` on the server and pre-mount, then the live value.
|
|
14
|
+
*/
|
|
15
|
+
export declare function RelativeTime({ timestamp, placeholder, fallback, refreshIntervalMs, }: RelativeTimeProps): import("react/jsx-runtime").JSX.Element;
|
|
16
|
+
//# sourceMappingURL=relative-time.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"relative-time.d.ts","sourceRoot":"","sources":["../../../src/react/components/relative-time.tsx"],"names":[],"mappings":"AAKA,MAAM,WAAW,iBAAiB;IAChC,wEAAwE;IACxE,SAAS,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,GAAG,SAAS,GAAG,IAAI,CAAC;IACrD,8EAA8E;IAC9E,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,uFAAuF;IACvF,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,2EAA2E;IAC3E,iBAAiB,CAAC,EAAE,MAAM,CAAC;CAC5B;AAED;;;GAGG;AACH,wBAAgB,YAAY,CAAC,EAC3B,SAAS,EACT,WAAmB,EACnB,QAAwB,EACxB,iBAA0B,GAC3B,EAAE,iBAAiB,2CA8BnB"}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { Fragment as _Fragment, jsx as _jsx } from "react/jsx-runtime";
|
|
3
|
+
import * as React from "react";
|
|
4
|
+
import { formatDistanceToNowStrict, toDate } from "@ttt-productions/firebase-helpers";
|
|
5
|
+
/**
|
|
6
|
+
* SSR-safe relative-time component.
|
|
7
|
+
* Renders `placeholder` on the server and pre-mount, then the live value.
|
|
8
|
+
*/
|
|
9
|
+
export function RelativeTime({ timestamp, placeholder = "...", fallback = "a while ago", refreshIntervalMs = 60_000, }) {
|
|
10
|
+
const [value, setValue] = React.useState(placeholder);
|
|
11
|
+
React.useEffect(() => {
|
|
12
|
+
let date = null;
|
|
13
|
+
if (timestamp instanceof Date) {
|
|
14
|
+
date = timestamp;
|
|
15
|
+
}
|
|
16
|
+
else if (typeof timestamp === "number") {
|
|
17
|
+
date = toDate(timestamp);
|
|
18
|
+
}
|
|
19
|
+
else if (typeof timestamp === "string") {
|
|
20
|
+
date = new Date(timestamp);
|
|
21
|
+
}
|
|
22
|
+
else if (timestamp && typeof timestamp === "object" && "toDate" in timestamp) {
|
|
23
|
+
date = toDate(timestamp);
|
|
24
|
+
}
|
|
25
|
+
if (!date || isNaN(date.getTime())) {
|
|
26
|
+
setValue(fallback);
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
setValue(formatDistanceToNowStrict(date, { addSuffix: true }));
|
|
30
|
+
if (refreshIntervalMs <= 0)
|
|
31
|
+
return;
|
|
32
|
+
const interval = setInterval(() => {
|
|
33
|
+
setValue(formatDistanceToNowStrict(date, { addSuffix: true }));
|
|
34
|
+
}, refreshIntervalMs);
|
|
35
|
+
return () => clearInterval(interval);
|
|
36
|
+
}, [timestamp, fallback, refreshIntervalMs]);
|
|
37
|
+
return _jsx(_Fragment, { children: value });
|
|
38
|
+
}
|
|
39
|
+
//# sourceMappingURL=relative-time.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"relative-time.js","sourceRoot":"","sources":["../../../src/react/components/relative-time.tsx"],"names":[],"mappings":"AAAA,YAAY,CAAC;;AAEb,OAAO,KAAK,KAAK,MAAM,OAAO,CAAC;AAC/B,OAAO,EAAE,yBAAyB,EAAE,MAAM,EAAE,MAAM,mCAAmC,CAAC;AAatF;;;GAGG;AACH,MAAM,UAAU,YAAY,CAAC,EAC3B,SAAS,EACT,WAAW,GAAG,KAAK,EACnB,QAAQ,GAAG,aAAa,EACxB,iBAAiB,GAAG,MAAM,GACR;IAClB,MAAM,CAAC,KAAK,EAAE,QAAQ,CAAC,GAAG,KAAK,CAAC,QAAQ,CAAS,WAAW,CAAC,CAAC;IAE9D,KAAK,CAAC,SAAS,CAAC,GAAG,EAAE;QACnB,IAAI,IAAI,GAAgB,IAAI,CAAC;QAC7B,IAAI,SAAS,YAAY,IAAI,EAAE,CAAC;YAC9B,IAAI,GAAG,SAAS,CAAC;QACnB,CAAC;aAAM,IAAI,OAAO,SAAS,KAAK,QAAQ,EAAE,CAAC;YACzC,IAAI,GAAG,MAAM,CAAC,SAAS,CAAC,CAAC;QAC3B,CAAC;aAAM,IAAI,OAAO,SAAS,KAAK,QAAQ,EAAE,CAAC;YACzC,IAAI,GAAG,IAAI,IAAI,CAAC,SAAS,CAAC,CAAC;QAC7B,CAAC;aAAM,IAAI,SAAS,IAAI,OAAO,SAAS,KAAK,QAAQ,IAAI,QAAQ,IAAI,SAAS,EAAE,CAAC;YAC/E,IAAI,GAAG,MAAM,CAAC,SAAS,CAAC,CAAC;QAC3B,CAAC;QAED,IAAI,CAAC,IAAI,IAAI,KAAK,CAAC,IAAI,CAAC,OAAO,EAAE,CAAC,EAAE,CAAC;YACnC,QAAQ,CAAC,QAAQ,CAAC,CAAC;YACnB,OAAO;QACT,CAAC;QAED,QAAQ,CAAC,yBAAyB,CAAC,IAAI,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC,CAAC;QAE/D,IAAI,iBAAiB,IAAI,CAAC;YAAE,OAAO;QACnC,MAAM,QAAQ,GAAG,WAAW,CAAC,GAAG,EAAE;YAChC,QAAQ,CAAC,yBAAyB,CAAC,IAAI,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC,CAAC;QACjE,CAAC,EAAE,iBAAiB,CAAC,CAAC;QACtB,OAAO,GAAG,EAAE,CAAC,aAAa,CAAC,QAAQ,CAAC,CAAC;IACvC,CAAC,EAAE,CAAC,SAAS,EAAE,QAAQ,EAAE,iBAAiB,CAAC,CAAC,CAAC;IAE7C,OAAO,4BAAG,KAAK,GAAI,CAAC;AACtB,CAAC"}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
export interface ScrollToTopButtonProps {
|
|
3
|
+
/** Scroll threshold (px) before the button appears. Default 400. */
|
|
4
|
+
threshold?: number;
|
|
5
|
+
/** Aria label. Default "Scroll to top". */
|
|
6
|
+
ariaLabel?: string;
|
|
7
|
+
/** Optional extra classes for the button. */
|
|
8
|
+
className?: string;
|
|
9
|
+
/** Optional override for the icon. */
|
|
10
|
+
icon?: React.ReactNode;
|
|
11
|
+
}
|
|
12
|
+
export declare function ScrollToTopButton({ threshold, ariaLabel, className, icon, }: ScrollToTopButtonProps): import("react/jsx-runtime").JSX.Element;
|
|
13
|
+
//# sourceMappingURL=scroll-to-top-button.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"scroll-to-top-button.d.ts","sourceRoot":"","sources":["../../../src/react/components/scroll-to-top-button.tsx"],"names":[],"mappings":"AAEA,OAAO,KAAK,KAAK,MAAM,OAAO,CAAC;AAK/B,MAAM,WAAW,sBAAsB;IACrC,oEAAoE;IACpE,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,2CAA2C;IAC3C,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,6CAA6C;IAC7C,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,sCAAsC;IACtC,IAAI,CAAC,EAAE,KAAK,CAAC,SAAS,CAAC;CACxB;AAED,wBAAgB,iBAAiB,CAAC,EAChC,SAAe,EACf,SAA2B,EAC3B,SAAS,EACT,IAAI,GACL,EAAE,sBAAsB,2CA0BxB"}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
3
|
+
import * as React from "react";
|
|
4
|
+
import { ChevronUp } from "lucide-react";
|
|
5
|
+
import { Button } from "./button.js";
|
|
6
|
+
import { cn } from "../../lib/utils.js";
|
|
7
|
+
export function ScrollToTopButton({ threshold = 400, ariaLabel = "Scroll to top", className, icon, }) {
|
|
8
|
+
const [isVisible, setIsVisible] = React.useState(false);
|
|
9
|
+
React.useEffect(() => {
|
|
10
|
+
const toggle = () => setIsVisible(window.scrollY > threshold);
|
|
11
|
+
toggle();
|
|
12
|
+
window.addEventListener("scroll", toggle, { passive: true });
|
|
13
|
+
return () => window.removeEventListener("scroll", toggle);
|
|
14
|
+
}, [threshold]);
|
|
15
|
+
const onClick = () => window.scrollTo({ top: 0, behavior: "smooth" });
|
|
16
|
+
return (_jsx(Button, { size: "icon", onClick: onClick, "aria-label": ariaLabel, className: cn("fixed bottom-24 left-1/2 -translate-x-1/2 z-50 rounded-full shadow-lg transition-opacity duration-300", isVisible ? "opacity-100" : "opacity-0 pointer-events-none", className), children: icon ?? _jsx(ChevronUp, { className: "h-5 w-5" }) }));
|
|
17
|
+
}
|
|
18
|
+
//# sourceMappingURL=scroll-to-top-button.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"scroll-to-top-button.js","sourceRoot":"","sources":["../../../src/react/components/scroll-to-top-button.tsx"],"names":[],"mappings":"AAAA,YAAY,CAAC;;AAEb,OAAO,KAAK,KAAK,MAAM,OAAO,CAAC;AAC/B,OAAO,EAAE,SAAS,EAAE,MAAM,cAAc,CAAC;AACzC,OAAO,EAAE,MAAM,EAAE,MAAM,aAAa,CAAC;AACrC,OAAO,EAAE,EAAE,EAAE,MAAM,oBAAoB,CAAC;AAaxC,MAAM,UAAU,iBAAiB,CAAC,EAChC,SAAS,GAAG,GAAG,EACf,SAAS,GAAG,eAAe,EAC3B,SAAS,EACT,IAAI,GACmB;IACvB,MAAM,CAAC,SAAS,EAAE,YAAY,CAAC,GAAG,KAAK,CAAC,QAAQ,CAAC,KAAK,CAAC,CAAC;IAExD,KAAK,CAAC,SAAS,CAAC,GAAG,EAAE;QACnB,MAAM,MAAM,GAAG,GAAG,EAAE,CAAC,YAAY,CAAC,MAAM,CAAC,OAAO,GAAG,SAAS,CAAC,CAAC;QAC9D,MAAM,EAAE,CAAC;QACT,MAAM,CAAC,gBAAgB,CAAC,QAAQ,EAAE,MAAM,EAAE,EAAE,OAAO,EAAE,IAAI,EAAE,CAAC,CAAC;QAC7D,OAAO,GAAG,EAAE,CAAC,MAAM,CAAC,mBAAmB,CAAC,QAAQ,EAAE,MAAM,CAAC,CAAC;IAC5D,CAAC,EAAE,CAAC,SAAS,CAAC,CAAC,CAAC;IAEhB,MAAM,OAAO,GAAG,GAAG,EAAE,CAAC,MAAM,CAAC,QAAQ,CAAC,EAAE,GAAG,EAAE,CAAC,EAAE,QAAQ,EAAE,QAAQ,EAAE,CAAC,CAAC;IAEtE,OAAO,CACL,KAAC,MAAM,IACL,IAAI,EAAC,MAAM,EACX,OAAO,EAAE,OAAO,gBACJ,SAAS,EACrB,SAAS,EAAE,EAAE,CACX,uGAAuG,EACvG,SAAS,CAAC,CAAC,CAAC,aAAa,CAAC,CAAC,CAAC,+BAA+B,EAC3D,SAAS,CACV,YAEA,IAAI,IAAI,KAAC,SAAS,IAAC,SAAS,EAAC,SAAS,GAAG,GACnC,CACV,CAAC;AACJ,CAAC"}
|
package/dist/react/index.d.ts
CHANGED
|
@@ -41,4 +41,12 @@ export { SearchDropdown } from "./components/search-dropdown.js";
|
|
|
41
41
|
export type { MaxWidthOption, ScreenAdaptiveViewProps } from "./components/layout/screen-adaptive-view.js";
|
|
42
42
|
export { ScreenAdaptiveView } from "./components/layout/screen-adaptive-view.js";
|
|
43
43
|
export { useMediaQuery } from "./hooks/use-media-query.js";
|
|
44
|
+
export { RelativeTime } from "./components/relative-time.js";
|
|
45
|
+
export type { RelativeTimeProps } from "./components/relative-time.js";
|
|
46
|
+
export { EndOfListIndicator } from "./components/end-of-list-indicator.js";
|
|
47
|
+
export type { EndOfListIndicatorProps } from "./components/end-of-list-indicator.js";
|
|
48
|
+
export { ScrollToTopButton } from "./components/scroll-to-top-button.js";
|
|
49
|
+
export type { ScrollToTopButtonProps } from "./components/scroll-to-top-button.js";
|
|
50
|
+
export { ChunkErrorRecovery } from "./components/chunk-error-recovery.js";
|
|
51
|
+
export type { ChunkErrorRecoveryProps } from "./components/chunk-error-recovery.js";
|
|
44
52
|
//# sourceMappingURL=index.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/react/index.ts"],"names":[],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/react/index.ts"],"names":[],"mappings":"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"}
|
package/dist/react/index.js
CHANGED
|
@@ -36,4 +36,9 @@ export { SearchDropdown } from "./components/search-dropdown.js";
|
|
|
36
36
|
export { ScreenAdaptiveView } from "./components/layout/screen-adaptive-view.js";
|
|
37
37
|
// Hooks
|
|
38
38
|
export { useMediaQuery } from "./hooks/use-media-query.js";
|
|
39
|
+
// Shared building blocks
|
|
40
|
+
export { RelativeTime } from "./components/relative-time.js";
|
|
41
|
+
export { EndOfListIndicator } from "./components/end-of-list-indicator.js";
|
|
42
|
+
export { ScrollToTopButton } from "./components/scroll-to-top-button.js";
|
|
43
|
+
export { ChunkErrorRecovery } from "./components/chunk-error-recovery.js";
|
|
39
44
|
//# sourceMappingURL=index.js.map
|
package/dist/react/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/react/index.ts"],"names":[],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/react/index.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ttt-productions/ui-core",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"description": "Shared UI components for TTT Productions projects",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -48,7 +48,8 @@
|
|
|
48
48
|
"react": ">=19.0.0",
|
|
49
49
|
"react-dom": ">=19.0.0",
|
|
50
50
|
"react-hook-form": ">=7.0.0",
|
|
51
|
-
"@hookform/resolvers": ">=3.0.0"
|
|
51
|
+
"@hookform/resolvers": ">=3.0.0",
|
|
52
|
+
"@ttt-productions/firebase-helpers": "*"
|
|
52
53
|
},
|
|
53
54
|
"dependencies": {
|
|
54
55
|
"@radix-ui/react-accordion": "^1.2.12",
|
|
@@ -81,7 +82,8 @@
|
|
|
81
82
|
"@types/react-dom": "^19.0.0",
|
|
82
83
|
"react": "^19.2.0",
|
|
83
84
|
"react-dom": "^19.2.0",
|
|
84
|
-
"typescript": "^5.8.3"
|
|
85
|
+
"typescript": "^5.8.3",
|
|
86
|
+
"@ttt-productions/firebase-helpers": "*"
|
|
85
87
|
},
|
|
86
88
|
"author": "DJ (TTT Productions)",
|
|
87
89
|
"license": "MIT"
|
package/src/index.ts
CHANGED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Compact number formatter:
|
|
3
|
+
* < 10,000 → "1234"
|
|
4
|
+
* 10,000 ≤ n < 1,000,000 → "12.3k" (trailing ".0" stripped)
|
|
5
|
+
* ≥ 1,000,000 → "1.2M" (trailing ".0" stripped)
|
|
6
|
+
*/
|
|
7
|
+
export function formatLargeNumber(num: number): string {
|
|
8
|
+
if (num >= 1_000_000) {
|
|
9
|
+
const formatted = (num / 1_000_000).toFixed(1);
|
|
10
|
+
return (formatted.endsWith(".0") ? formatted.slice(0, -2) : formatted) + "M";
|
|
11
|
+
}
|
|
12
|
+
if (num >= 10_000) {
|
|
13
|
+
const formatted = (num / 1000).toFixed(1);
|
|
14
|
+
return (formatted.endsWith(".0") ? formatted.slice(0, -2) : formatted) + "k";
|
|
15
|
+
}
|
|
16
|
+
return num.toString();
|
|
17
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import * as React from "react";
|
|
4
|
+
import { Loader2 } from "lucide-react";
|
|
5
|
+
|
|
6
|
+
export interface ChunkErrorRecoveryProps {
|
|
7
|
+
children: React.ReactNode;
|
|
8
|
+
/** Custom message shown during recovery reload. */
|
|
9
|
+
loadingMessage?: React.ReactNode;
|
|
10
|
+
/**
|
|
11
|
+
* Predicate to decide whether an error event indicates a stale-chunk failure.
|
|
12
|
+
* Defaults to matching "Failed to load chunk" in the message.
|
|
13
|
+
*/
|
|
14
|
+
isChunkError?: (event: ErrorEvent) => boolean;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function defaultIsChunkError(event: ErrorEvent): boolean {
|
|
18
|
+
const msg = event.message ?? "";
|
|
19
|
+
if (msg.includes("Failed to load chunk")) return true;
|
|
20
|
+
const nested =
|
|
21
|
+
event.error && typeof event.error === "object"
|
|
22
|
+
? (event.error as { message?: string }).message
|
|
23
|
+
: undefined;
|
|
24
|
+
return typeof nested === "string" && nested.includes("Failed to load chunk");
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function ChunkErrorRecovery({
|
|
28
|
+
children,
|
|
29
|
+
loadingMessage = "Updating…",
|
|
30
|
+
isChunkError = defaultIsChunkError,
|
|
31
|
+
}: ChunkErrorRecoveryProps) {
|
|
32
|
+
const [hasError, setHasError] = React.useState(false);
|
|
33
|
+
|
|
34
|
+
React.useEffect(() => {
|
|
35
|
+
const handler = (event: ErrorEvent) => {
|
|
36
|
+
if (!isChunkError(event)) return;
|
|
37
|
+
// eslint-disable-next-line no-console
|
|
38
|
+
console.error("Stale chunk detected — reloading.");
|
|
39
|
+
setHasError(true);
|
|
40
|
+
window.location.reload();
|
|
41
|
+
event.preventDefault();
|
|
42
|
+
event.stopPropagation();
|
|
43
|
+
};
|
|
44
|
+
window.addEventListener("error", handler);
|
|
45
|
+
return () => window.removeEventListener("error", handler);
|
|
46
|
+
}, [isChunkError]);
|
|
47
|
+
|
|
48
|
+
if (hasError) {
|
|
49
|
+
return (
|
|
50
|
+
<div className="flex flex-col h-screen w-screen items-center justify-center gap-4 bg-background">
|
|
51
|
+
<Loader2 className="h-10 w-10 animate-spin" />
|
|
52
|
+
<p className="text-muted-foreground">{loadingMessage}</p>
|
|
53
|
+
</div>
|
|
54
|
+
);
|
|
55
|
+
}
|
|
56
|
+
return <>{children}</>;
|
|
57
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
import { CheckCircle2 } from "lucide-react";
|
|
3
|
+
import { cn } from "../../lib/utils.js";
|
|
4
|
+
|
|
5
|
+
export interface EndOfListIndicatorProps {
|
|
6
|
+
message?: string;
|
|
7
|
+
className?: string;
|
|
8
|
+
icon?: React.ReactNode;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function EndOfListIndicator({
|
|
12
|
+
message = "You've reached the end!",
|
|
13
|
+
className,
|
|
14
|
+
icon,
|
|
15
|
+
}: EndOfListIndicatorProps) {
|
|
16
|
+
return (
|
|
17
|
+
<div
|
|
18
|
+
className={cn(
|
|
19
|
+
"flex flex-col items-center gap-2 py-8 text-center text-muted-foreground",
|
|
20
|
+
className,
|
|
21
|
+
)}
|
|
22
|
+
>
|
|
23
|
+
{icon ?? <CheckCircle2 className="h-6 w-6 text-emerald-500" />}
|
|
24
|
+
<p className="text-sm">{message}</p>
|
|
25
|
+
</div>
|
|
26
|
+
);
|
|
27
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import * as React from "react";
|
|
4
|
+
import { formatDistanceToNowStrict, toDate } from "@ttt-productions/firebase-helpers";
|
|
5
|
+
|
|
6
|
+
export interface RelativeTimeProps {
|
|
7
|
+
/** Number (ms or seconds), ISO string, Firestore Timestamp, or Date. */
|
|
8
|
+
timestamp: number | string | Date | undefined | null;
|
|
9
|
+
/** Fallback rendered before client hydration completes. Defaults to "...". */
|
|
10
|
+
placeholder?: string;
|
|
11
|
+
/** Fallback rendered when the timestamp can't be parsed. Defaults to "a while ago". */
|
|
12
|
+
fallback?: string;
|
|
13
|
+
/** Auto-refresh interval in ms. Defaults to 60000. Set to 0 to disable. */
|
|
14
|
+
refreshIntervalMs?: number;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* SSR-safe relative-time component.
|
|
19
|
+
* Renders `placeholder` on the server and pre-mount, then the live value.
|
|
20
|
+
*/
|
|
21
|
+
export function RelativeTime({
|
|
22
|
+
timestamp,
|
|
23
|
+
placeholder = "...",
|
|
24
|
+
fallback = "a while ago",
|
|
25
|
+
refreshIntervalMs = 60_000,
|
|
26
|
+
}: RelativeTimeProps) {
|
|
27
|
+
const [value, setValue] = React.useState<string>(placeholder);
|
|
28
|
+
|
|
29
|
+
React.useEffect(() => {
|
|
30
|
+
let date: Date | null = null;
|
|
31
|
+
if (timestamp instanceof Date) {
|
|
32
|
+
date = timestamp;
|
|
33
|
+
} else if (typeof timestamp === "number") {
|
|
34
|
+
date = toDate(timestamp);
|
|
35
|
+
} else if (typeof timestamp === "string") {
|
|
36
|
+
date = new Date(timestamp);
|
|
37
|
+
} else if (timestamp && typeof timestamp === "object" && "toDate" in timestamp) {
|
|
38
|
+
date = toDate(timestamp);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
if (!date || isNaN(date.getTime())) {
|
|
42
|
+
setValue(fallback);
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
setValue(formatDistanceToNowStrict(date, { addSuffix: true }));
|
|
47
|
+
|
|
48
|
+
if (refreshIntervalMs <= 0) return;
|
|
49
|
+
const interval = setInterval(() => {
|
|
50
|
+
setValue(formatDistanceToNowStrict(date, { addSuffix: true }));
|
|
51
|
+
}, refreshIntervalMs);
|
|
52
|
+
return () => clearInterval(interval);
|
|
53
|
+
}, [timestamp, fallback, refreshIntervalMs]);
|
|
54
|
+
|
|
55
|
+
return <>{value}</>;
|
|
56
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import * as React from "react";
|
|
4
|
+
import { ChevronUp } from "lucide-react";
|
|
5
|
+
import { Button } from "./button.js";
|
|
6
|
+
import { cn } from "../../lib/utils.js";
|
|
7
|
+
|
|
8
|
+
export interface ScrollToTopButtonProps {
|
|
9
|
+
/** Scroll threshold (px) before the button appears. Default 400. */
|
|
10
|
+
threshold?: number;
|
|
11
|
+
/** Aria label. Default "Scroll to top". */
|
|
12
|
+
ariaLabel?: string;
|
|
13
|
+
/** Optional extra classes for the button. */
|
|
14
|
+
className?: string;
|
|
15
|
+
/** Optional override for the icon. */
|
|
16
|
+
icon?: React.ReactNode;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function ScrollToTopButton({
|
|
20
|
+
threshold = 400,
|
|
21
|
+
ariaLabel = "Scroll to top",
|
|
22
|
+
className,
|
|
23
|
+
icon,
|
|
24
|
+
}: ScrollToTopButtonProps) {
|
|
25
|
+
const [isVisible, setIsVisible] = React.useState(false);
|
|
26
|
+
|
|
27
|
+
React.useEffect(() => {
|
|
28
|
+
const toggle = () => setIsVisible(window.scrollY > threshold);
|
|
29
|
+
toggle();
|
|
30
|
+
window.addEventListener("scroll", toggle, { passive: true });
|
|
31
|
+
return () => window.removeEventListener("scroll", toggle);
|
|
32
|
+
}, [threshold]);
|
|
33
|
+
|
|
34
|
+
const onClick = () => window.scrollTo({ top: 0, behavior: "smooth" });
|
|
35
|
+
|
|
36
|
+
return (
|
|
37
|
+
<Button
|
|
38
|
+
size="icon"
|
|
39
|
+
onClick={onClick}
|
|
40
|
+
aria-label={ariaLabel}
|
|
41
|
+
className={cn(
|
|
42
|
+
"fixed bottom-24 left-1/2 -translate-x-1/2 z-50 rounded-full shadow-lg transition-opacity duration-300",
|
|
43
|
+
isVisible ? "opacity-100" : "opacity-0 pointer-events-none",
|
|
44
|
+
className,
|
|
45
|
+
)}
|
|
46
|
+
>
|
|
47
|
+
{icon ?? <ChevronUp className="h-5 w-5" />}
|
|
48
|
+
</Button>
|
|
49
|
+
);
|
|
50
|
+
}
|
package/src/react/index.ts
CHANGED
|
@@ -48,3 +48,16 @@ export { ScreenAdaptiveView } from "./components/layout/screen-adaptive-view.js"
|
|
|
48
48
|
|
|
49
49
|
// Hooks
|
|
50
50
|
export { useMediaQuery } from "./hooks/use-media-query.js";
|
|
51
|
+
|
|
52
|
+
// Shared building blocks
|
|
53
|
+
export { RelativeTime } from "./components/relative-time.js";
|
|
54
|
+
export type { RelativeTimeProps } from "./components/relative-time.js";
|
|
55
|
+
|
|
56
|
+
export { EndOfListIndicator } from "./components/end-of-list-indicator.js";
|
|
57
|
+
export type { EndOfListIndicatorProps } from "./components/end-of-list-indicator.js";
|
|
58
|
+
|
|
59
|
+
export { ScrollToTopButton } from "./components/scroll-to-top-button.js";
|
|
60
|
+
export type { ScrollToTopButtonProps } from "./components/scroll-to-top-button.js";
|
|
61
|
+
|
|
62
|
+
export { ChunkErrorRecovery } from "./components/chunk-error-recovery.js";
|
|
63
|
+
export type { ChunkErrorRecoveryProps } from "./components/chunk-error-recovery.js";
|