hazo_ui 6.16.0 → 6.19.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.
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/lib/utils.ts","../src/lib/remembered_state.ts","../src/lib/announce.ts","../src/lib/scroll_restore.ts"],"names":[],"mappings":";;;AAWO,SAAS,MAAM,MAAA,EAAsB;AAC1C,EAAA,OAAO,OAAA,CAAQ,IAAA,CAAK,MAAM,CAAC,CAAA;AAC7B;;;ACIO,SAAS,oBAAuB,GAAA,EAAuB;AAC5D,EAAA,IAAI,OAAO,MAAA,KAAW,WAAA,EAAa,OAAO,IAAA;AAC1C,EAAA,IAAI;AACF,IAAA,MAAM,GAAA,GAAM,MAAA,CAAO,YAAA,CAAa,OAAA,CAAQ,GAAG,CAAA;AAC3C,IAAA,OAAO,GAAA,GAAO,IAAA,CAAK,KAAA,CAAM,GAAG,CAAA,GAAU,IAAA;AAAA,EACxC,CAAA,CAAA,MAAQ;AACN,IAAA,OAAO,IAAA;AAAA,EACT;AACF;AAaO,SAAS,oBAAA,CAAwB,KAAa,QAAA,EAAmB;AACtE,EAAA,IAAI,OAAO,WAAW,WAAA,EAAa;AACnC,EAAA,IAAI;AACF,IAAA,MAAA,CAAO,aAAa,OAAA,CAAQ,GAAA,EAAK,IAAA,CAAK,SAAA,CAAU,QAAQ,CAAC,CAAA;AAAA,EAC3D,CAAA,CAAA,MAAQ;AAAA,EAER;AACF;AAeO,SAAS,WAAA,CAAY,QAAgB,SAAA,EAA4B;AACtE,EAAA,IAAI;AACF,IAAA,OAAO,IAAI,eAAA,CAAgB,MAAM,CAAA,CAAE,IAAI,SAAS,CAAA;AAAA,EAClD,CAAA,CAAA,MAAQ;AACN,IAAA,OAAO,KAAA;AAAA,EACT;AACF;;;ACtCO,IAAM,sBAAA,GAAyB;AAuB/B,SAAS,QAAA,CACd,OAAA,EACA,QAAA,GAA6B,QAAA,EAC7B,OAAA,EACM;AACN,EAAA,IAAI,OAAO,WAAW,WAAA,EAAa;AACnC,EAAA,MAAM,OAAA,GAAU,QAAQ,IAAA,EAAK;AAC7B,EAAA,IAAI,CAAC,OAAA,EAAS;AACd,EAAA,MAAM,SAAA,GAAY,SAAS,SAAA,IAAa,sBAAA;AACxC,EAAA,MAAA,CAAO,aAAA;AAAA,IACL,IAAI,YAA8B,SAAA,EAAW;AAAA,MAC3C,MAAA,EAAQ,EAAE,OAAA,EAAS,OAAA,EAAS,QAAA;AAAS,KACtC;AAAA,GACH;AACF;;;ACrDO,IAAM,6BAAA,GAAgC;AAStC,SAAS,qBAAA,CAAsB,MAAA,EAAgB,MAAA,EAAgB,QAAA,EAA0B;AAC9F,EAAA,OAAO,CAAA,EAAG,MAAM,CAAA,CAAA,EAAI,MAAM,IAAI,QAAQ,CAAA,CAAA;AACxC;AAUO,SAAS,aAAA,CAAc,KAAa,OAAA,EAAuB;AAChE,EAAA,IAAI,OAAO,WAAW,WAAA,EAAa;AACnC,EAAA,IAAI;AACF,IAAA,MAAA,CAAO,YAAA,CAAa,QAAQ,GAAA,EAAK,MAAA,CAAO,KAAK,KAAA,CAAM,OAAO,CAAC,CAAC,CAAA;AAAA,EAC9D,CAAA,CAAA,MAAQ;AAAA,EAER;AACF;AASO,SAAS,cAAc,GAAA,EAA4B;AACxD,EAAA,IAAI,OAAO,MAAA,KAAW,WAAA,EAAa,OAAO,IAAA;AAC1C,EAAA,IAAI;AACF,IAAA,MAAM,GAAA,GAAM,MAAA,CAAO,YAAA,CAAa,OAAA,CAAQ,GAAG,CAAA;AAC3C,IAAA,IAAI,CAAC,KAAK,OAAO,IAAA;AACjB,IAAA,MAAM,CAAA,GAAI,MAAA,CAAO,QAAA,CAAS,GAAA,EAAK,EAAE,CAAA;AACjC,IAAA,OAAO,MAAA,CAAO,QAAA,CAAS,CAAC,CAAA,GAAI,CAAA,GAAI,IAAA;AAAA,EAClC,CAAA,CAAA,MAAQ;AACN,IAAA,OAAO,IAAA;AAAA,EACT;AACF","file":"index.utils.js","sourcesContent":["// Utility functions for hazo_ui component library\n// Provides className merging utility (cn function)\n\nimport { type ClassValue, clsx } from \"clsx\";\nimport { twMerge } from \"tailwind-merge\";\n\n/**\n * Merges class names using clsx and tailwind-merge\n * @param inputs - Class values to merge\n * @returns Merged class string\n */\nexport function cn(...inputs: ClassValue[]) {\n return twMerge(clsx(inputs));\n}\n\n/**\n * Sanitizes a user/admin-supplied href against a scheme allowlist to prevent\n * `javascript:`/`data:`/`vbscript:` URI XSS when the value is rendered into an\n * `<a href>`. Allows http(s), mailto, and same-origin absolute paths (`/…` but\n * not protocol-relative `//…`). Returns `undefined` for anything else, so the\n * anchor renders with no href rather than an active malicious one.\n * @param url - Candidate href (may be undefined)\n * @returns The original url if allowed, otherwise undefined\n */\nexport function safeHref(url?: string): string | undefined {\n if (!url) return undefined;\n const trimmed = url.trim();\n const lower = trimmed.toLowerCase();\n if (\n lower.startsWith(\"http://\") ||\n lower.startsWith(\"https://\") ||\n lower.startsWith(\"mailto:\")\n ) {\n return trimmed;\n }\n // Same-origin absolute path, but not protocol-relative \"//host\".\n if (trimmed.startsWith(\"/\") && !trimmed.startsWith(\"//\")) {\n return trimmed;\n }\n return undefined;\n}\n\n","// remembered_state.ts — SSR-safe localStorage read/write/URL-param helpers\n// backing `useRememberedState` (src/hooks/use_remembered_state.ts). Pure,\n// framework-free functions (no React) so they're independently testable and\n// usable outside the hook. No \"use client\" directive here — this module is\n// re-exported from both the main client bundle (`hazo_ui`) and the pure\n// server-safe subpath (`hazo_ui/utils`), same dual-export pattern as `cn`.\n\n/**\n * Reads and JSON-parses the remembered value stored under `key`. SSR-safe —\n * returns `null` on the server, when nothing is stored, or when the stored\n * value is corrupt/unparsable.\n *\n * @param key - localStorage key to read.\n *\n * @example\n * const seed = readRememberedState<MySeed>(\"myapp:tool:cgt\");\n */\nexport function readRememberedState<T>(key: string): T | null {\n if (typeof window === \"undefined\") return null;\n try {\n const raw = window.localStorage.getItem(key);\n return raw ? (JSON.parse(raw) as T) : null;\n } catch {\n return null;\n }\n}\n\n/**\n * JSON-stringifies and writes `snapshot` under `key`. SSR-safe — no-ops on\n * the server. Quota / private-mode errors are swallowed; losing a memory\n * write is non-fatal.\n *\n * @param key - localStorage key to write.\n * @param snapshot - Serialisable value to remember.\n *\n * @example\n * writeRememberedState(\"myapp:tool:cgt\", { proceeds: 1000 });\n */\nexport function writeRememberedState<T>(key: string, snapshot: T): void {\n if (typeof window === \"undefined\") return;\n try {\n window.localStorage.setItem(key, JSON.stringify(snapshot));\n } catch {\n /* quota / private-mode — non-fatal */\n }\n}\n\n/**\n * True when `paramName` is present in the given search string (e.g.\n * `window.location.search`). Generic form of the \"an explicit shared-link /\n * saved-record query param always wins over remembered state\" precedence\n * check — callers supply the param name (e.g. `\"restore\"`); this module\n * never hardcodes one.\n *\n * @param search - A URL search string, e.g. `window.location.search`.\n * @param paramName - The query param to look for.\n *\n * @example\n * hasUrlParam(\"?restore=abc123\", \"restore\"); // true\n */\nexport function hasUrlParam(search: string, paramName: string): boolean {\n try {\n return new URLSearchParams(search).has(paramName);\n } catch {\n return false;\n }\n}\n","// announce.ts — SSR-safe screen-reader announcement bus. Dispatches a\n// `window` CustomEvent that `LiveRegion` (../components/live_region/)\n// listens for. Pure, framework-free (no React) so it's independently\n// testable and usable outside a component. No \"use client\" directive here\n// — this module is re-exported from both the main client bundle\n// (`hazo_ui`) and the pure server-safe subpath (`hazo_ui/utils`), same\n// dual-export pattern as `cn` and the `remembered_state` helpers.\n//\n// Ported from Kinstripe's `src/lib/a11y/announce.ts`, which hardcoded the\n// event name to `\"kinstripe:announce\"`. Here the event name is caller-\n// configurable (`options.eventName`) so multiple apps — or multiple\n// `LiveRegion` instances on the same page — don't collide on a single\n// hardcoded channel; it defaults to `DEFAULT_ANNOUNCE_EVENT` for\n// single-app usage.\n\nexport type AnnouncePriority = \"polite\" | \"assertive\";\n\nexport interface LiveAnnouncement {\n message: string;\n priority: AnnouncePriority;\n}\n\nexport interface AnnounceOptions {\n /** Custom-event name the paired `LiveRegion` listens on. Must match the `eventName` it was mounted with. Default `DEFAULT_ANNOUNCE_EVENT`. */\n eventName?: string;\n}\n\n/** Default `window` CustomEvent name used when `announce()` / `LiveRegion` are not given an explicit `eventName`. */\nexport const DEFAULT_ANNOUNCE_EVENT = \"hazo_ui:announce\";\n\n/**\n * Dispatch a screen-reader announcement via a `window` CustomEvent. Safe to\n * call from anywhere in the client tree — no-ops during SSR or before a\n * `LiveRegion` has mounted (fire-and-forget; there's no queue/replay).\n *\n * Use `\"polite\"` (default) for ambient updates (\"Sarah joined the canvas\",\n * \"2 new notifications\"). Use `\"assertive\"` only for things the user must\n * hear immediately (errors that interrupt a task).\n *\n * @param message - Text to announce. Empty / whitespace-only messages are dropped (no dispatch).\n * @param priority - `\"polite\"` (default) or `\"assertive\"`.\n * @param options.eventName - Custom-event name to dispatch on. Default `DEFAULT_ANNOUNCE_EVENT`.\n *\n * @example\n * announce(\"2 new notifications\");\n * announce(\"Save failed — check your connection\", \"assertive\");\n *\n * @example\n * // namespaced channel — must match the eventName the LiveRegion was mounted with\n * announce(\"Saved\", \"polite\", { eventName: \"myapp:announce\" });\n */\nexport function announce(\n message: string,\n priority: AnnouncePriority = \"polite\",\n options?: AnnounceOptions,\n): void {\n if (typeof window === \"undefined\") return;\n const trimmed = message.trim();\n if (!trimmed) return;\n const eventName = options?.eventName ?? DEFAULT_ANNOUNCE_EVENT;\n window.dispatchEvent(\n new CustomEvent<LiveAnnouncement>(eventName, {\n detail: { message: trimmed, priority },\n }),\n );\n}\n","// scroll_restore.ts — SSR-safe localStorage read/write helpers backing\n// `useScrollRestore` (../hooks/use_scroll_restore.ts). Pure, framework-free\n// functions (no React) so they're independently testable and usable\n// outside the hook — same split as `remembered_state.ts` /\n// `useRememberedState`. No \"use client\" directive here.\n//\n// Ported from Kinstripe's `src/lib/hooks/useScrollRestore.ts`, which\n// hardcoded the key prefix to `\"kinstripe.scrollPos\"`. Here the prefix is a\n// parameter (`buildScrollRestoreKey`) / hook option, so the package stays\n// app-agnostic.\n\n/** Default localStorage key prefix used when no `prefix` option is given. */\nexport const DEFAULT_SCROLL_RESTORE_PREFIX = \"hazo_ui:scrollPos\";\n\n/**\n * Composes the localStorage key from a prefix, tree id, and view name.\n *\n * @example\n * buildScrollRestoreKey(\"hazo_ui:scrollPos\", \"tree-42\", \"canvas\");\n * // -> \"hazo_ui:scrollPos.tree-42.canvas\"\n */\nexport function buildScrollRestoreKey(prefix: string, treeId: string, viewName: string): string {\n return `${prefix}.${treeId}.${viewName}`;\n}\n\n/**\n * Persists a scroll position (rounded to the nearest pixel) under `key`.\n * SSR-safe — no-ops on the server. Quota / private-mode errors are\n * swallowed; a failed write is non-fatal.\n *\n * @param key - localStorage key (see `buildScrollRestoreKey`).\n * @param scrollY - Scroll offset in pixels.\n */\nexport function saveScrollPos(key: string, scrollY: number): void {\n if (typeof window === \"undefined\") return;\n try {\n window.localStorage.setItem(key, String(Math.round(scrollY)));\n } catch {\n /* quota exceeded — silently drop */\n }\n}\n\n/**\n * Reads back the scroll position stored under `key`. SSR-safe — returns\n * `null` on the server, when nothing is stored, or when the stored value\n * isn't a finite number.\n *\n * @param key - localStorage key (see `buildScrollRestoreKey`).\n */\nexport function loadScrollPos(key: string): number | null {\n if (typeof window === \"undefined\") return null;\n try {\n const raw = window.localStorage.getItem(key);\n if (!raw) return null;\n const n = Number.parseInt(raw, 10);\n return Number.isFinite(n) ? n : null;\n } catch {\n return null;\n }\n}\n"]}
1
+ {"version":3,"sources":["../src/lib/utils.ts","../src/lib/remembered_state.ts","../src/lib/announce.ts","../src/lib/scroll_restore.ts","../src/lib/trend_stats.ts"],"names":[],"mappings":"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","file":"index.utils.js","sourcesContent":["// Utility functions for hazo_ui component library\n// Provides className merging utility (cn function)\n\nimport { type ClassValue, clsx } from \"clsx\";\nimport { twMerge } from \"tailwind-merge\";\n\n/**\n * Merges class names using clsx and tailwind-merge\n * @param inputs - Class values to merge\n * @returns Merged class string\n */\nexport function cn(...inputs: ClassValue[]) {\n return twMerge(clsx(inputs));\n}\n\n/**\n * Sanitizes a user/admin-supplied href against a scheme allowlist to prevent\n * `javascript:`/`data:`/`vbscript:` URI XSS when the value is rendered into an\n * `<a href>`. Allows http(s), mailto, and same-origin absolute paths (`/…` but\n * not protocol-relative `//…`). Returns `undefined` for anything else, so the\n * anchor renders with no href rather than an active malicious one.\n * @param url - Candidate href (may be undefined)\n * @returns The original url if allowed, otherwise undefined\n */\nexport function safeHref(url?: string): string | undefined {\n if (!url) return undefined;\n const trimmed = url.trim();\n const lower = trimmed.toLowerCase();\n if (\n lower.startsWith(\"http://\") ||\n lower.startsWith(\"https://\") ||\n lower.startsWith(\"mailto:\")\n ) {\n return trimmed;\n }\n // Same-origin absolute path, but not protocol-relative \"//host\".\n if (trimmed.startsWith(\"/\") && !trimmed.startsWith(\"//\")) {\n return trimmed;\n }\n return undefined;\n}\n\n","// remembered_state.ts — SSR-safe localStorage read/write/URL-param helpers\n// backing `useRememberedState` (src/hooks/use_remembered_state.ts). Pure,\n// framework-free functions (no React) so they're independently testable and\n// usable outside the hook. No \"use client\" directive here — this module is\n// re-exported from both the main client bundle (`hazo_ui`) and the pure\n// server-safe subpath (`hazo_ui/utils`), same dual-export pattern as `cn`.\n\n/**\n * Reads and JSON-parses the remembered value stored under `key`. SSR-safe —\n * returns `null` on the server, when nothing is stored, or when the stored\n * value is corrupt/unparsable.\n *\n * @param key - localStorage key to read.\n *\n * @example\n * const seed = readRememberedState<MySeed>(\"myapp:tool:cgt\");\n */\nexport function readRememberedState<T>(key: string): T | null {\n if (typeof window === \"undefined\") return null;\n try {\n const raw = window.localStorage.getItem(key);\n return raw ? (JSON.parse(raw) as T) : null;\n } catch {\n return null;\n }\n}\n\n/**\n * JSON-stringifies and writes `snapshot` under `key`. SSR-safe — no-ops on\n * the server. Quota / private-mode errors are swallowed; losing a memory\n * write is non-fatal.\n *\n * @param key - localStorage key to write.\n * @param snapshot - Serialisable value to remember.\n *\n * @example\n * writeRememberedState(\"myapp:tool:cgt\", { proceeds: 1000 });\n */\nexport function writeRememberedState<T>(key: string, snapshot: T): void {\n if (typeof window === \"undefined\") return;\n try {\n window.localStorage.setItem(key, JSON.stringify(snapshot));\n } catch {\n /* quota / private-mode — non-fatal */\n }\n}\n\n/**\n * True when `paramName` is present in the given search string (e.g.\n * `window.location.search`). Generic form of the \"an explicit shared-link /\n * saved-record query param always wins over remembered state\" precedence\n * check — callers supply the param name (e.g. `\"restore\"`); this module\n * never hardcodes one.\n *\n * @param search - A URL search string, e.g. `window.location.search`.\n * @param paramName - The query param to look for.\n *\n * @example\n * hasUrlParam(\"?restore=abc123\", \"restore\"); // true\n */\nexport function hasUrlParam(search: string, paramName: string): boolean {\n try {\n return new URLSearchParams(search).has(paramName);\n } catch {\n return false;\n }\n}\n","// announce.ts — SSR-safe screen-reader announcement bus. Dispatches a\n// `window` CustomEvent that `LiveRegion` (../components/live_region/)\n// listens for. Pure, framework-free (no React) so it's independently\n// testable and usable outside a component. No \"use client\" directive here\n// — this module is re-exported from both the main client bundle\n// (`hazo_ui`) and the pure server-safe subpath (`hazo_ui/utils`), same\n// dual-export pattern as `cn` and the `remembered_state` helpers.\n//\n// Ported from Kinstripe's `src/lib/a11y/announce.ts`, which hardcoded the\n// event name to `\"kinstripe:announce\"`. Here the event name is caller-\n// configurable (`options.eventName`) so multiple apps — or multiple\n// `LiveRegion` instances on the same page — don't collide on a single\n// hardcoded channel; it defaults to `DEFAULT_ANNOUNCE_EVENT` for\n// single-app usage.\n\nexport type AnnouncePriority = \"polite\" | \"assertive\";\n\nexport interface LiveAnnouncement {\n message: string;\n priority: AnnouncePriority;\n}\n\nexport interface AnnounceOptions {\n /** Custom-event name the paired `LiveRegion` listens on. Must match the `eventName` it was mounted with. Default `DEFAULT_ANNOUNCE_EVENT`. */\n eventName?: string;\n}\n\n/** Default `window` CustomEvent name used when `announce()` / `LiveRegion` are not given an explicit `eventName`. */\nexport const DEFAULT_ANNOUNCE_EVENT = \"hazo_ui:announce\";\n\n/**\n * Dispatch a screen-reader announcement via a `window` CustomEvent. Safe to\n * call from anywhere in the client tree — no-ops during SSR or before a\n * `LiveRegion` has mounted (fire-and-forget; there's no queue/replay).\n *\n * Use `\"polite\"` (default) for ambient updates (\"Sarah joined the canvas\",\n * \"2 new notifications\"). Use `\"assertive\"` only for things the user must\n * hear immediately (errors that interrupt a task).\n *\n * @param message - Text to announce. Empty / whitespace-only messages are dropped (no dispatch).\n * @param priority - `\"polite\"` (default) or `\"assertive\"`.\n * @param options.eventName - Custom-event name to dispatch on. Default `DEFAULT_ANNOUNCE_EVENT`.\n *\n * @example\n * announce(\"2 new notifications\");\n * announce(\"Save failed — check your connection\", \"assertive\");\n *\n * @example\n * // namespaced channel — must match the eventName the LiveRegion was mounted with\n * announce(\"Saved\", \"polite\", { eventName: \"myapp:announce\" });\n */\nexport function announce(\n message: string,\n priority: AnnouncePriority = \"polite\",\n options?: AnnounceOptions,\n): void {\n if (typeof window === \"undefined\") return;\n const trimmed = message.trim();\n if (!trimmed) return;\n const eventName = options?.eventName ?? DEFAULT_ANNOUNCE_EVENT;\n window.dispatchEvent(\n new CustomEvent<LiveAnnouncement>(eventName, {\n detail: { message: trimmed, priority },\n }),\n );\n}\n","// scroll_restore.ts — SSR-safe localStorage read/write helpers backing\n// `useScrollRestore` (../hooks/use_scroll_restore.ts). Pure, framework-free\n// functions (no React) so they're independently testable and usable\n// outside the hook — same split as `remembered_state.ts` /\n// `useRememberedState`. No \"use client\" directive here.\n//\n// Ported from Kinstripe's `src/lib/hooks/useScrollRestore.ts`, which\n// hardcoded the key prefix to `\"kinstripe.scrollPos\"`. Here the prefix is a\n// parameter (`buildScrollRestoreKey`) / hook option, so the package stays\n// app-agnostic.\n\n/** Default localStorage key prefix used when no `prefix` option is given. */\nexport const DEFAULT_SCROLL_RESTORE_PREFIX = \"hazo_ui:scrollPos\";\n\n/**\n * Composes the localStorage key from a prefix, tree id, and view name.\n *\n * @example\n * buildScrollRestoreKey(\"hazo_ui:scrollPos\", \"tree-42\", \"canvas\");\n * // -> \"hazo_ui:scrollPos.tree-42.canvas\"\n */\nexport function buildScrollRestoreKey(prefix: string, treeId: string, viewName: string): string {\n return `${prefix}.${treeId}.${viewName}`;\n}\n\n/**\n * Persists a scroll position (rounded to the nearest pixel) under `key`.\n * SSR-safe — no-ops on the server. Quota / private-mode errors are\n * swallowed; a failed write is non-fatal.\n *\n * @param key - localStorage key (see `buildScrollRestoreKey`).\n * @param scrollY - Scroll offset in pixels.\n */\nexport function saveScrollPos(key: string, scrollY: number): void {\n if (typeof window === \"undefined\") return;\n try {\n window.localStorage.setItem(key, String(Math.round(scrollY)));\n } catch {\n /* quota exceeded — silently drop */\n }\n}\n\n/**\n * Reads back the scroll position stored under `key`. SSR-safe — returns\n * `null` on the server, when nothing is stored, or when the stored value\n * isn't a finite number.\n *\n * @param key - localStorage key (see `buildScrollRestoreKey`).\n */\nexport function loadScrollPos(key: string): number | null {\n if (typeof window === \"undefined\") return null;\n try {\n const raw = window.localStorage.getItem(key);\n if (!raw) return null;\n const n = Number.parseInt(raw, 10);\n return Number.isFinite(n) ? n : null;\n } catch {\n return null;\n }\n}\n","// file_description: Pure, dependency-free helpers to compute trend stats\n// (rolling-window counts + % change vs the prior equal period, plus a YTD\n// monthly series) from a list of timestamped records. Client-safe.\n\nexport interface TrendWindow {\n /** stable key: 'all' | '7d' | '30d' | '90d' | '180d' | 'ytd' */\n key: string;\n /** short display label for the card */\n label: string;\n /** items within the window ('all' = total count) */\n count: number;\n /** items in the immediately-preceding equal-length period; null for 'all' */\n prevCount: number | null;\n /** rounded % change vs prevCount; null when prevCount is null or 0 */\n pctChange: number | null;\n}\n\nexport interface TrendChartPoint {\n /** short label, e.g. 'Jan' */\n label: string;\n /** items in that period */\n count: number;\n}\n\nexport interface TrendStats {\n total: number;\n windows: TrendWindow[];\n ytdMonthly: TrendChartPoint[];\n}\n\nexport interface TrendStatsOptions {\n /** Label for the \"all time\" card. Default: \"All time\". */\n totalLabel?: string;\n /** Custom rolling windows. Default: 7d/30d/90d/180d. */\n windows?: ReadonlyArray<{ key: string; label: string; days: number }>;\n}\n\nconst DAY_MS = 86_400_000;\nconst MONTHS = [\n \"Jan\", \"Feb\", \"Mar\", \"Apr\", \"May\", \"Jun\",\n \"Jul\", \"Aug\", \"Sep\", \"Oct\", \"Nov\", \"Dec\",\n];\n\nfunction pctOf(count: number, prev: number | null): number | null {\n if (prev === null || prev === 0) return null;\n return Math.round(((count - prev) / prev) * 100);\n}\n\nconst DEFAULT_WINDOWS: ReadonlyArray<{ key: string; label: string; days: number }> = [\n { key: \"7d\", label: \"7 days\", days: 7 },\n { key: \"30d\", label: \"30 days\", days: 30 },\n { key: \"90d\", label: \"3 months\", days: 90 },\n { key: \"180d\", label: \"6 months\", days: 180 },\n];\n\n/**\n * Compute trend stats from timestamped records.\n *\n * Rolling windows (7/30/90/180 days) compare against the immediately-preceding\n * equal-length period. YTD compares against the prior year's same-length\n * period. Rows with missing/invalid timestamps are excluded from all counts.\n */\nexport function computeTrendStats(\n items: ReadonlyArray<{ created_at?: string | null }>,\n now: Date = new Date(),\n options?: TrendStatsOptions,\n): TrendStats {\n const nowMs = now.getTime();\n\n const times: number[] = [];\n for (const item of items) {\n if (!item || !item.created_at) continue;\n const t = new Date(item.created_at).getTime();\n if (!Number.isNaN(t)) times.push(t);\n }\n\n const countBetween = (startMs: number, endMs: number): number =>\n times.reduce((n, t) => (t >= startMs && t < endMs ? n + 1 : n), 0);\n\n const total = times.length;\n const rollingDefs = options?.windows ?? DEFAULT_WINDOWS;\n\n const windows: TrendWindow[] = [\n { key: \"all\", label: options?.totalLabel ?? \"All time\", count: total, prevCount: null, pctChange: null },\n ];\n\n for (const w of rollingDefs) {\n const span = w.days * DAY_MS;\n const count = countBetween(nowMs - span, nowMs);\n const prevCount = countBetween(nowMs - 2 * span, nowMs - span);\n windows.push({\n key: w.key,\n label: w.label,\n count,\n prevCount,\n pctChange: pctOf(count, prevCount),\n });\n }\n\n const ytdStart = new Date(now.getFullYear(), 0, 1).getTime();\n const elapsed = nowMs - ytdStart;\n const ytdCount = countBetween(ytdStart, nowMs);\n const prevYtdStart = new Date(now.getFullYear() - 1, 0, 1).getTime();\n const prevYtdCount = countBetween(prevYtdStart, prevYtdStart + elapsed);\n windows.push({\n key: \"ytd\",\n label: \"YTD\",\n count: ytdCount,\n prevCount: prevYtdCount,\n pctChange: pctOf(ytdCount, prevYtdCount),\n });\n\n const ytdMonthly: TrendChartPoint[] = [];\n const year = now.getFullYear();\n for (let m = 0; m <= now.getMonth(); m++) {\n const monthStart = new Date(year, m, 1).getTime();\n const monthEnd = new Date(year, m + 1, 1).getTime();\n ytdMonthly.push({ label: MONTHS[m], count: countBetween(monthStart, monthEnd) });\n }\n\n return { total, windows, ytdMonthly };\n}\n"]}
@@ -89,4 +89,48 @@ declare const DEFAULT_ANNOUNCE_EVENT = "hazo_ui:announce";
89
89
  */
90
90
  declare function announce(message: string, priority?: AnnouncePriority, options?: AnnounceOptions): void;
91
91
 
92
- export { type AnnounceOptions as A, DEFAULT_ANNOUNCE_EVENT as D, type LiveAnnouncement as L, type AnnouncePriority as a, announce as b, cn as c, hasUrlParam as h, readRememberedState as r, safeHref as s, writeRememberedState as w };
92
+ interface TrendWindow {
93
+ /** stable key: 'all' | '7d' | '30d' | '90d' | '180d' | 'ytd' */
94
+ key: string;
95
+ /** short display label for the card */
96
+ label: string;
97
+ /** items within the window ('all' = total count) */
98
+ count: number;
99
+ /** items in the immediately-preceding equal-length period; null for 'all' */
100
+ prevCount: number | null;
101
+ /** rounded % change vs prevCount; null when prevCount is null or 0 */
102
+ pctChange: number | null;
103
+ }
104
+ interface TrendChartPoint {
105
+ /** short label, e.g. 'Jan' */
106
+ label: string;
107
+ /** items in that period */
108
+ count: number;
109
+ }
110
+ interface TrendStats {
111
+ total: number;
112
+ windows: TrendWindow[];
113
+ ytdMonthly: TrendChartPoint[];
114
+ }
115
+ interface TrendStatsOptions {
116
+ /** Label for the "all time" card. Default: "All time". */
117
+ totalLabel?: string;
118
+ /** Custom rolling windows. Default: 7d/30d/90d/180d. */
119
+ windows?: ReadonlyArray<{
120
+ key: string;
121
+ label: string;
122
+ days: number;
123
+ }>;
124
+ }
125
+ /**
126
+ * Compute trend stats from timestamped records.
127
+ *
128
+ * Rolling windows (7/30/90/180 days) compare against the immediately-preceding
129
+ * equal-length period. YTD compares against the prior year's same-length
130
+ * period. Rows with missing/invalid timestamps are excluded from all counts.
131
+ */
132
+ declare function computeTrendStats(items: ReadonlyArray<{
133
+ created_at?: string | null;
134
+ }>, now?: Date, options?: TrendStatsOptions): TrendStats;
135
+
136
+ export { type AnnounceOptions as A, DEFAULT_ANNOUNCE_EVENT as D, type LiveAnnouncement as L, type TrendChartPoint as T, type AnnouncePriority as a, type TrendStats as b, type TrendStatsOptions as c, type TrendWindow as d, announce as e, cn as f, computeTrendStats as g, hasUrlParam as h, readRememberedState as r, safeHref as s, writeRememberedState as w };
@@ -89,4 +89,48 @@ declare const DEFAULT_ANNOUNCE_EVENT = "hazo_ui:announce";
89
89
  */
90
90
  declare function announce(message: string, priority?: AnnouncePriority, options?: AnnounceOptions): void;
91
91
 
92
- export { type AnnounceOptions as A, DEFAULT_ANNOUNCE_EVENT as D, type LiveAnnouncement as L, type AnnouncePriority as a, announce as b, cn as c, hasUrlParam as h, readRememberedState as r, safeHref as s, writeRememberedState as w };
92
+ interface TrendWindow {
93
+ /** stable key: 'all' | '7d' | '30d' | '90d' | '180d' | 'ytd' */
94
+ key: string;
95
+ /** short display label for the card */
96
+ label: string;
97
+ /** items within the window ('all' = total count) */
98
+ count: number;
99
+ /** items in the immediately-preceding equal-length period; null for 'all' */
100
+ prevCount: number | null;
101
+ /** rounded % change vs prevCount; null when prevCount is null or 0 */
102
+ pctChange: number | null;
103
+ }
104
+ interface TrendChartPoint {
105
+ /** short label, e.g. 'Jan' */
106
+ label: string;
107
+ /** items in that period */
108
+ count: number;
109
+ }
110
+ interface TrendStats {
111
+ total: number;
112
+ windows: TrendWindow[];
113
+ ytdMonthly: TrendChartPoint[];
114
+ }
115
+ interface TrendStatsOptions {
116
+ /** Label for the "all time" card. Default: "All time". */
117
+ totalLabel?: string;
118
+ /** Custom rolling windows. Default: 7d/30d/90d/180d. */
119
+ windows?: ReadonlyArray<{
120
+ key: string;
121
+ label: string;
122
+ days: number;
123
+ }>;
124
+ }
125
+ /**
126
+ * Compute trend stats from timestamped records.
127
+ *
128
+ * Rolling windows (7/30/90/180 days) compare against the immediately-preceding
129
+ * equal-length period. YTD compares against the prior year's same-length
130
+ * period. Rows with missing/invalid timestamps are excluded from all counts.
131
+ */
132
+ declare function computeTrendStats(items: ReadonlyArray<{
133
+ created_at?: string | null;
134
+ }>, now?: Date, options?: TrendStatsOptions): TrendStats;
135
+
136
+ export { type AnnounceOptions as A, DEFAULT_ANNOUNCE_EVENT as D, type LiveAnnouncement as L, type TrendChartPoint as T, type AnnouncePriority as a, type TrendStats as b, type TrendStatsOptions as c, type TrendWindow as d, announce as e, cn as f, computeTrendStats as g, hasUrlParam as h, readRememberedState as r, safeHref as s, writeRememberedState as w };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "hazo_ui",
3
- "version": "6.16.0",
3
+ "version": "6.19.0",
4
4
  "description": "Set of UI components for common interaction elements in a SaaS app",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -49,7 +49,7 @@
49
49
  },
50
50
  "peerDependencies": {
51
51
  "hazo_core": "^2.4.1",
52
- "hazo_i18n": "^0.9.0",
52
+ "hazo_i18n": "^0.9.0 || ^0.15.0",
53
53
  "hazo_state": "^0.6.3",
54
54
  "hazo_theme": "^1.4.0",
55
55
  "react": "^18.0.0 || ^19.0.0",
@@ -133,7 +133,7 @@
133
133
  "@types/react": "^18.3.3",
134
134
  "@types/react-dom": "^18.3.0",
135
135
  "hazo_core": "^2.5.0",
136
- "hazo_theme": "^1.4.0",
136
+ "hazo_theme": "^1.4.2",
137
137
  "jest": "^30.2.0",
138
138
  "jest-environment-jsdom": "^30.2.0",
139
139
  "postcss": "^8.4.49",