@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.
Files changed (36) hide show
  1. package/dist/index.d.ts +1 -0
  2. package/dist/index.d.ts.map +1 -1
  3. package/dist/index.js +1 -0
  4. package/dist/index.js.map +1 -1
  5. package/dist/lib/format-numbers.d.ts +8 -0
  6. package/dist/lib/format-numbers.d.ts.map +1 -0
  7. package/dist/lib/format-numbers.js +18 -0
  8. package/dist/lib/format-numbers.js.map +1 -0
  9. package/dist/react/components/chunk-error-recovery.d.ts +13 -0
  10. package/dist/react/components/chunk-error-recovery.d.ts.map +1 -0
  11. package/dist/react/components/chunk-error-recovery.js +35 -0
  12. package/dist/react/components/chunk-error-recovery.js.map +1 -0
  13. package/dist/react/components/end-of-list-indicator.d.ts +8 -0
  14. package/dist/react/components/end-of-list-indicator.d.ts.map +1 -0
  15. package/dist/react/components/end-of-list-indicator.js +7 -0
  16. package/dist/react/components/end-of-list-indicator.js.map +1 -0
  17. package/dist/react/components/relative-time.d.ts +16 -0
  18. package/dist/react/components/relative-time.d.ts.map +1 -0
  19. package/dist/react/components/relative-time.js +39 -0
  20. package/dist/react/components/relative-time.js.map +1 -0
  21. package/dist/react/components/scroll-to-top-button.d.ts +13 -0
  22. package/dist/react/components/scroll-to-top-button.d.ts.map +1 -0
  23. package/dist/react/components/scroll-to-top-button.js +18 -0
  24. package/dist/react/components/scroll-to-top-button.js.map +1 -0
  25. package/dist/react/index.d.ts +8 -0
  26. package/dist/react/index.d.ts.map +1 -1
  27. package/dist/react/index.js +5 -0
  28. package/dist/react/index.js.map +1 -1
  29. package/package.json +5 -3
  30. package/src/index.ts +1 -0
  31. package/src/lib/format-numbers.ts +17 -0
  32. package/src/react/components/chunk-error-recovery.tsx +57 -0
  33. package/src/react/components/end-of-list-indicator.tsx +27 -0
  34. package/src/react/components/relative-time.tsx +56 -0
  35. package/src/react/components/scroll-to-top-button.tsx +50 -0
  36. package/src/react/index.ts +13 -0
package/dist/index.d.ts CHANGED
@@ -1,2 +1,3 @@
1
1
  export * from "./lib/utils.js";
2
+ export * from "./lib/format-numbers.js";
2
3
  //# sourceMappingURL=index.d.ts.map
@@ -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
@@ -1,3 +1,4 @@
1
1
  // Server-safe utilities only. React components and hooks live behind ./react.
2
2
  export * from "./lib/utils.js";
3
+ export * from "./lib/format-numbers.js";
3
4
  //# sourceMappingURL=index.js.map
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"}
@@ -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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"}
@@ -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
@@ -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.2.32",
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
@@ -1,2 +1,3 @@
1
1
  // Server-safe utilities only. React components and hooks live behind ./react.
2
2
  export * from "./lib/utils.js";
3
+ export * from "./lib/format-numbers.js";
@@ -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
+ }
@@ -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";