@unofficialbox/box-open-elements 0.5.0 → 0.7.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 (227) hide show
  1. package/README.md +27 -73
  2. package/dist/components/collections/table.d.ts +113 -3
  3. package/dist/components/collections/table.js +540 -37
  4. package/dist/components/feedback/due-badge.d.ts +38 -0
  5. package/dist/components/feedback/due-badge.js +168 -0
  6. package/dist/components/feedback/due-types.d.ts +32 -0
  7. package/dist/components/feedback/due-types.js +91 -0
  8. package/dist/components/feedback/progress-steps.d.ts +51 -5
  9. package/dist/components/feedback/progress-steps.js +200 -22
  10. package/dist/components/feedback/stage-path.d.ts +44 -0
  11. package/dist/components/feedback/stage-path.js +235 -0
  12. package/dist/components/forms/select.d.ts +21 -0
  13. package/dist/components/forms/select.js +100 -3
  14. package/dist/components/forms/text-field.d.ts +21 -0
  15. package/dist/components/forms/text-field.js +100 -1
  16. package/dist/components/layout/nav-sidebar.d.ts +14 -0
  17. package/dist/components/layout/nav-sidebar.js +49 -0
  18. package/dist/components/layout/split-view.d.ts +14 -0
  19. package/dist/components/layout/split-view.js +80 -1
  20. package/dist/components/overlays/command-palette.d.ts +62 -0
  21. package/dist/components/overlays/command-palette.js +537 -0
  22. package/dist/components/overlays/command-types.d.ts +90 -0
  23. package/dist/components/overlays/command-types.js +259 -0
  24. package/dist/components/overlays/drawer.d.ts +18 -0
  25. package/dist/components/overlays/drawer.js +135 -9
  26. package/dist/components/overlays/shortcuts-overlay.d.ts +48 -0
  27. package/dist/components/overlays/shortcuts-overlay.js +385 -0
  28. package/dist/components/overlays/tooltip.js +1 -1
  29. package/dist/core/index.d.ts +4 -0
  30. package/dist/core/index.js +2 -0
  31. package/dist/core/offset-window.d.ts +70 -0
  32. package/dist/core/offset-window.js +134 -0
  33. package/dist/core/virtualize.d.ts +71 -0
  34. package/dist/core/virtualize.js +67 -0
  35. package/dist/element-maps.d.ts +292 -0
  36. package/dist/element-maps.js +1 -0
  37. package/dist/entries/activity-density.d.ts +1 -0
  38. package/dist/entries/activity-density.js +1 -0
  39. package/dist/entries/agent-chat.d.ts +1 -0
  40. package/dist/entries/agent-chat.js +1 -0
  41. package/dist/entries/audit-log.d.ts +1 -0
  42. package/dist/entries/audit-log.js +1 -0
  43. package/dist/entries/command-palette.d.ts +1 -0
  44. package/dist/entries/command-palette.js +1 -0
  45. package/dist/entries/compare-view.d.ts +1 -0
  46. package/dist/entries/compare-view.js +1 -0
  47. package/dist/entries/diff-viewer.d.ts +1 -0
  48. package/dist/entries/diff-viewer.js +1 -0
  49. package/dist/entries/due-badge.d.ts +1 -0
  50. package/dist/entries/due-badge.js +1 -0
  51. package/dist/entries/form-wizard.d.ts +1 -0
  52. package/dist/entries/form-wizard.js +1 -0
  53. package/dist/entries/lineage-graph.d.ts +1 -0
  54. package/dist/entries/lineage-graph.js +1 -0
  55. package/dist/entries/notification-bell.d.ts +1 -0
  56. package/dist/entries/notification-bell.js +1 -0
  57. package/dist/entries/notification-inbox.d.ts +1 -0
  58. package/dist/entries/notification-inbox.js +1 -0
  59. package/dist/entries/provenance-strip.d.ts +1 -0
  60. package/dist/entries/provenance-strip.js +1 -0
  61. package/dist/entries/run-trace.d.ts +1 -0
  62. package/dist/entries/run-trace.js +1 -0
  63. package/dist/entries/shortcuts-overlay.d.ts +1 -0
  64. package/dist/entries/shortcuts-overlay.js +1 -0
  65. package/dist/entries/signature-ceremony.d.ts +1 -0
  66. package/dist/entries/signature-ceremony.js +1 -0
  67. package/dist/entries/stage-path.d.ts +1 -0
  68. package/dist/entries/stage-path.js +1 -0
  69. package/dist/entries/timeline.d.ts +1 -0
  70. package/dist/entries/timeline.js +1 -0
  71. package/dist/entries/version-graph.d.ts +1 -0
  72. package/dist/entries/version-graph.js +1 -0
  73. package/dist/entries/version-list.d.ts +1 -0
  74. package/dist/entries/version-list.js +1 -0
  75. package/dist/entries/wizard-summary.d.ts +1 -0
  76. package/dist/entries/wizard-summary.js +1 -0
  77. package/dist/entries/work-queue.d.ts +1 -0
  78. package/dist/entries/work-queue.js +1 -0
  79. package/dist/entries/workload-board.d.ts +1 -0
  80. package/dist/entries/workload-board.js +1 -0
  81. package/dist/index.d.ts +21 -0
  82. package/dist/index.js +21 -0
  83. package/dist/patterns/agent-chat/agent-chat.d.ts +75 -0
  84. package/dist/patterns/agent-chat/agent-chat.js +820 -0
  85. package/dist/patterns/agent-chat/controller.d.ts +27 -0
  86. package/dist/patterns/agent-chat/controller.js +182 -0
  87. package/dist/patterns/agent-chat/index.d.ts +3 -0
  88. package/dist/patterns/agent-chat/index.js +6 -0
  89. package/dist/patterns/agent-chat/types.d.ts +109 -0
  90. package/dist/patterns/agent-chat/types.js +1 -0
  91. package/dist/patterns/audit/activity-density.d.ts +68 -0
  92. package/dist/patterns/audit/activity-density.js +428 -0
  93. package/dist/patterns/audit/audit-log.d.ts +182 -0
  94. package/dist/patterns/audit/audit-log.js +1082 -0
  95. package/dist/patterns/audit/index.d.ts +3 -0
  96. package/dist/patterns/audit/index.js +7 -0
  97. package/dist/patterns/audit/shared.d.ts +10 -0
  98. package/dist/patterns/audit/shared.js +50 -0
  99. package/dist/patterns/audit/types.d.ts +143 -0
  100. package/dist/patterns/audit/types.js +327 -0
  101. package/dist/patterns/audit/window.d.ts +116 -0
  102. package/dist/patterns/audit/window.js +132 -0
  103. package/dist/patterns/content-explorer/actions/controller.js +12 -2
  104. package/dist/patterns/content-explorer/box-transport.js +117 -25
  105. package/dist/patterns/content-explorer/content-explorer.d.ts +13 -1
  106. package/dist/patterns/content-explorer/content-explorer.js +36 -3
  107. package/dist/patterns/content-explorer/contracts.d.ts +24 -1
  108. package/dist/patterns/content-explorer/contracts.js +68 -11
  109. package/dist/patterns/content-explorer/controller.d.ts +16 -1
  110. package/dist/patterns/content-explorer/controller.js +130 -4
  111. package/dist/patterns/content-explorer/navigation/controller.js +11 -9
  112. package/dist/patterns/content-explorer/types.d.ts +58 -1
  113. package/dist/patterns/content-picker/content-picker.d.ts +71 -0
  114. package/dist/patterns/content-picker/content-picker.js +747 -0
  115. package/dist/patterns/content-picker/controller.d.ts +38 -0
  116. package/dist/patterns/content-picker/controller.js +141 -0
  117. package/dist/patterns/content-picker/index.d.ts +3 -0
  118. package/dist/patterns/content-picker/index.js +6 -0
  119. package/dist/patterns/content-picker/types.d.ts +52 -0
  120. package/dist/patterns/content-picker/types.js +26 -0
  121. package/dist/patterns/content-sidebar/content-sidebar.d.ts +43 -0
  122. package/dist/patterns/content-sidebar/content-sidebar.js +293 -0
  123. package/dist/patterns/content-sidebar/index.d.ts +2 -0
  124. package/dist/patterns/content-sidebar/index.js +4 -0
  125. package/dist/patterns/content-sidebar/types.d.ts +18 -0
  126. package/dist/patterns/content-sidebar/types.js +20 -0
  127. package/dist/patterns/content-uploader/box-transport.d.ts +14 -0
  128. package/dist/patterns/content-uploader/box-transport.js +54 -0
  129. package/dist/patterns/content-uploader/content-uploader.d.ts +62 -0
  130. package/dist/patterns/content-uploader/content-uploader.js +559 -0
  131. package/dist/patterns/content-uploader/controller.d.ts +39 -0
  132. package/dist/patterns/content-uploader/controller.js +227 -0
  133. package/dist/patterns/content-uploader/index.d.ts +4 -0
  134. package/dist/patterns/content-uploader/index.js +8 -0
  135. package/dist/patterns/content-uploader/types.d.ts +120 -0
  136. package/dist/patterns/content-uploader/types.js +38 -0
  137. package/dist/patterns/diff/compare-view.d.ts +47 -0
  138. package/dist/patterns/diff/compare-view.js +276 -0
  139. package/dist/patterns/diff/diff-viewer.d.ts +42 -0
  140. package/dist/patterns/diff/diff-viewer.js +434 -0
  141. package/dist/patterns/diff/engine.d.ts +22 -0
  142. package/dist/patterns/diff/engine.js +276 -0
  143. package/dist/patterns/diff/index.d.ts +4 -0
  144. package/dist/patterns/diff/index.js +8 -0
  145. package/dist/patterns/diff/types.d.ts +69 -0
  146. package/dist/patterns/diff/types.js +24 -0
  147. package/dist/patterns/form-wizard/controller.d.ts +39 -0
  148. package/dist/patterns/form-wizard/controller.js +157 -0
  149. package/dist/patterns/form-wizard/form-wizard.d.ts +63 -0
  150. package/dist/patterns/form-wizard/form-wizard.js +468 -0
  151. package/dist/patterns/form-wizard/index.d.ts +4 -0
  152. package/dist/patterns/form-wizard/index.js +8 -0
  153. package/dist/patterns/form-wizard/types.d.ts +109 -0
  154. package/dist/patterns/form-wizard/types.js +83 -0
  155. package/dist/patterns/form-wizard/wizard-summary.d.ts +62 -0
  156. package/dist/patterns/form-wizard/wizard-summary.js +324 -0
  157. package/dist/patterns/internal/safe-href.d.ts +17 -0
  158. package/dist/patterns/internal/safe-href.js +17 -0
  159. package/dist/patterns/lineage/index.d.ts +3 -0
  160. package/dist/patterns/lineage/index.js +6 -0
  161. package/dist/patterns/lineage/lineage-graph.d.ts +30 -0
  162. package/dist/patterns/lineage/lineage-graph.js +434 -0
  163. package/dist/patterns/lineage/provenance-strip.d.ts +22 -0
  164. package/dist/patterns/lineage/provenance-strip.js +194 -0
  165. package/dist/patterns/lineage/types.d.ts +53 -0
  166. package/dist/patterns/lineage/types.js +49 -0
  167. package/dist/patterns/notifications/index.d.ts +3 -0
  168. package/dist/patterns/notifications/index.js +6 -0
  169. package/dist/patterns/notifications/notification-bell.d.ts +38 -0
  170. package/dist/patterns/notifications/notification-bell.js +198 -0
  171. package/dist/patterns/notifications/notification-inbox.d.ts +46 -0
  172. package/dist/patterns/notifications/notification-inbox.js +537 -0
  173. package/dist/patterns/notifications/types.d.ts +61 -0
  174. package/dist/patterns/notifications/types.js +108 -0
  175. package/dist/patterns/preview/annotation-inspector.d.ts +25 -6
  176. package/dist/patterns/preview/annotation-inspector.js +239 -94
  177. package/dist/patterns/preview/annotation-thread.d.ts +25 -5
  178. package/dist/patterns/preview/annotation-thread.js +233 -82
  179. package/dist/patterns/preview/annotation-toolbar.d.ts +20 -6
  180. package/dist/patterns/preview/annotation-toolbar.js +162 -96
  181. package/dist/patterns/preview/content-preview-adapter.d.ts +10 -1
  182. package/dist/patterns/preview/content-preview-adapter.js +26 -1
  183. package/dist/patterns/preview/preview-element.d.ts +15 -2
  184. package/dist/patterns/preview/preview-element.js +223 -52
  185. package/dist/patterns/preview/provider-adapter.d.ts +47 -0
  186. package/dist/patterns/preview/provider-adapter.js +17 -0
  187. package/dist/patterns/run/index.d.ts +2 -0
  188. package/dist/patterns/run/index.js +4 -0
  189. package/dist/patterns/run/run-trace.d.ts +40 -0
  190. package/dist/patterns/run/run-trace.js +441 -0
  191. package/dist/patterns/run/types.d.ts +66 -0
  192. package/dist/patterns/run/types.js +152 -0
  193. package/dist/patterns/signature/index.d.ts +2 -0
  194. package/dist/patterns/signature/index.js +4 -0
  195. package/dist/patterns/signature/signature-ceremony.d.ts +33 -0
  196. package/dist/patterns/signature/signature-ceremony.js +317 -0
  197. package/dist/patterns/signature/types.d.ts +68 -0
  198. package/dist/patterns/signature/types.js +89 -0
  199. package/dist/patterns/timeline/index.d.ts +2 -0
  200. package/dist/patterns/timeline/index.js +4 -0
  201. package/dist/patterns/timeline/timeline.d.ts +27 -0
  202. package/dist/patterns/timeline/timeline.js +430 -0
  203. package/dist/patterns/timeline/types.d.ts +36 -0
  204. package/dist/patterns/timeline/types.js +27 -0
  205. package/dist/patterns/versions/graph-layout.d.ts +43 -0
  206. package/dist/patterns/versions/graph-layout.js +159 -0
  207. package/dist/patterns/versions/index.d.ts +4 -0
  208. package/dist/patterns/versions/index.js +8 -0
  209. package/dist/patterns/versions/shared.d.ts +4 -0
  210. package/dist/patterns/versions/shared.js +13 -0
  211. package/dist/patterns/versions/types.d.ts +37 -0
  212. package/dist/patterns/versions/types.js +39 -0
  213. package/dist/patterns/versions/version-graph.d.ts +36 -0
  214. package/dist/patterns/versions/version-graph.js +438 -0
  215. package/dist/patterns/versions/version-list.d.ts +39 -0
  216. package/dist/patterns/versions/version-list.js +407 -0
  217. package/dist/patterns/work-queue/controller.d.ts +26 -0
  218. package/dist/patterns/work-queue/controller.js +150 -0
  219. package/dist/patterns/work-queue/index.d.ts +4 -0
  220. package/dist/patterns/work-queue/index.js +7 -0
  221. package/dist/patterns/work-queue/types.d.ts +128 -0
  222. package/dist/patterns/work-queue/types.js +45 -0
  223. package/dist/patterns/work-queue/work-queue.d.ts +48 -0
  224. package/dist/patterns/work-queue/work-queue.js +534 -0
  225. package/dist/patterns/work-queue/workload-board.d.ts +53 -0
  226. package/dist/patterns/work-queue/workload-board.js +608 -0
  227. package/package.json +89 -5
@@ -0,0 +1,3 @@
1
+ export * from "./types.js";
2
+ export * from "./audit-log.js";
3
+ export * from "./activity-density.js";
@@ -0,0 +1,7 @@
1
+ // Aggregation model over the timeline event contract: grouping, faceting,
2
+ // CSV export, and the calendar-density window — all pure and DOM-free.
3
+ export * from "./types.js";
4
+ // Grouped, faceted, drillable audit surface
5
+ export * from "./audit-log.js";
6
+ // Managerial throughput heatmap
7
+ export * from "./activity-density.js";
@@ -0,0 +1,10 @@
1
+ export declare const escapeHtml: (value: string) => string;
2
+ export declare const auditToneColor: (tone: string) => string;
3
+ /**
4
+ * Format an event instant for a row. Rendered in UTC to match the UTC day
5
+ * grouping — a row must never appear to sit outside the day section holding
6
+ * it, which is exactly what a locale-local time would produce near midnight.
7
+ */
8
+ export declare const formatAuditTimestamp: (value: string | undefined) => string;
9
+ /** Reference time for deterministic day labels; falls back to the wall clock. */
10
+ export declare const resolveReferenceTime: (raw: string | null) => Date;
@@ -0,0 +1,50 @@
1
+ import { formatUtcDay } from "./types.js";
2
+ // Internal helpers shared by the two audit projections. Kept in one place so
3
+ // escaping and formatting can never drift between the log and the density
4
+ // strip — they render the same records.
5
+ export const escapeHtml = (value) => value
6
+ .replaceAll("&", "&")
7
+ .replaceAll("<", "&lt;")
8
+ .replaceAll(">", "&gt;")
9
+ .replaceAll('"', "&quot;")
10
+ .replaceAll("'", "&#39;");
11
+ export const auditToneColor = (tone) => {
12
+ switch (tone) {
13
+ case "brand":
14
+ return "var(--boe-token-surface-surface-brand, #0061d5)";
15
+ case "success":
16
+ return "var(--boe-token-surface-status-surface-success, #26a27b)";
17
+ case "warning":
18
+ return "var(--boe-token-surface-status-surface-warning, #f5b31b)";
19
+ case "error":
20
+ return "var(--boe-token-surface-status-surface-error, #ed3757)";
21
+ default:
22
+ return "var(--boe-token-text-text-secondary, #6f6f6f)";
23
+ }
24
+ };
25
+ const pad = (value) => String(value).padStart(2, "0");
26
+ /**
27
+ * Format an event instant for a row. Rendered in UTC to match the UTC day
28
+ * grouping — a row must never appear to sit outside the day section holding
29
+ * it, which is exactly what a locale-local time would produce near midnight.
30
+ */
31
+ export const formatAuditTimestamp = (value) => {
32
+ if (!value) {
33
+ return "";
34
+ }
35
+ const date = new Date(value);
36
+ if (Number.isNaN(date.getTime())) {
37
+ return "";
38
+ }
39
+ return `${formatUtcDay(date)}, ${pad(date.getUTCHours())}:${pad(date.getUTCMinutes())} UTC`;
40
+ };
41
+ /** Reference time for deterministic day labels; falls back to the wall clock. */
42
+ export const resolveReferenceTime = (raw) => {
43
+ if (raw) {
44
+ const parsed = new Date(raw);
45
+ if (!Number.isNaN(parsed.getTime())) {
46
+ return parsed;
47
+ }
48
+ }
49
+ return new Date();
50
+ };
@@ -0,0 +1,143 @@
1
+ import type { TimelineEvent } from "../timeline/types.js";
2
+ /**
3
+ * The audit layer reads the same append-only event contract `box-timeline`
4
+ * renders. The timeline is the flat feed; this pattern is the aggregation,
5
+ * faceting, drill-down, and export layer over identical records, so a host
6
+ * feeds both surfaces from one source without a second model.
7
+ */
8
+ export type AuditEvent = TimelineEvent;
9
+ /** The dimension sections are grouped by. */
10
+ export type AuditGroupBy = "day" | "actor" | "action";
11
+ export declare const AUDIT_GROUP_BY_VALUES: readonly AuditGroupBy[];
12
+ export declare const AUDIT_GROUP_BY_LABELS: Record<AuditGroupBy, string>;
13
+ export declare const resolveAuditGroupBy: (value: string | null | undefined) => AuditGroupBy;
14
+ /**
15
+ * Facet selection. Every facet is an exact match except `from`/`to`, which
16
+ * bound the event instant inclusively.
17
+ */
18
+ export interface AuditFacets {
19
+ /** Matches `actor.name` exactly. */
20
+ actor?: string;
21
+ /** Matches `action` exactly. */
22
+ action?: string;
23
+ /** The workflow-run drill-down. */
24
+ correlationId?: string;
25
+ /** Inclusive ISO lower bound. */
26
+ from?: string;
27
+ /** Inclusive ISO upper bound. */
28
+ to?: string;
29
+ }
30
+ export interface AuditGroup {
31
+ /** Stable identity for the section — the collapse state is keyed by it. */
32
+ key: string;
33
+ label: string;
34
+ events: AuditEvent[];
35
+ count: number;
36
+ /** Distinct attributed actors in the group; 0 when every event is unattributed. */
37
+ actorCount: number;
38
+ }
39
+ export interface AuditFacetValue {
40
+ value: string;
41
+ label: string;
42
+ count: number;
43
+ }
44
+ export interface AuditFacetSummary {
45
+ actors: AuditFacetValue[];
46
+ actions: AuditFacetValue[];
47
+ }
48
+ export declare const AUDIT_UNDATED_KEY = "undated";
49
+ export declare const AUDIT_UNATTRIBUTED_KEY = "";
50
+ /**
51
+ * The event's instant in epoch milliseconds, or `null` when it carries no
52
+ * usable timestamp. Undated events are a real case in imported audit data —
53
+ * they sort last rather than being dropped.
54
+ */
55
+ export declare const auditEventInstant: (event: AuditEvent) => number | null;
56
+ /**
57
+ * The UTC calendar day (`YYYY-MM-DD`) an event belongs to, or `null` when it
58
+ * is undated. Day keys and day labels are both derived in UTC so a viewer's
59
+ * timezone can never split one audit day across two sections. A host that
60
+ * wants local-day semantics offsets the timestamps it supplies.
61
+ */
62
+ export declare const resolveAuditDay: (event: AuditEvent) => string | null;
63
+ /**
64
+ * The one absolute-day formatter — `Aug 13, 2026`, always in UTC. Accepts a
65
+ * `YYYY-MM-DD` key or an instant. Every audit surface formats days through
66
+ * this, so a rendered date can never disagree with the UTC day key that
67
+ * grouped it. An unparseable key is returned unchanged.
68
+ */
69
+ export declare const formatUtcDay: (value: Date | string) => string;
70
+ /** The short UTC month name — `Aug` — for sparse calendar column headers. */
71
+ export declare const formatUtcMonth: (value: Date | string) => string;
72
+ /**
73
+ * Label a `YYYY-MM-DD` day key, with Today/Yesterday relative labels resolved
74
+ * against the caller's reference time so the output is deterministic rather
75
+ * than clock-dependent.
76
+ */
77
+ export declare const formatAuditDay: (day: string, now?: Date) => string;
78
+ /**
79
+ * Apply the facet selection. An empty string is "not selected" rather than a
80
+ * match on empty — the same meaning `""` carries as an unset attribute and as
81
+ * a select's all-values option, so the DOM and the engine never disagree.
82
+ *
83
+ * A date bound excludes undated events: an event with no timestamp cannot be
84
+ * shown to fall inside the window, and an audit surface must not imply it does.
85
+ */
86
+ export declare const filterAuditEvents: (events: readonly AuditEvent[], facets: AuditFacets) => AuditEvent[];
87
+ /** True when any facet narrows the set — drives the Clear-filters affordance. */
88
+ export declare const hasAuditFacets: (facets: AuditFacets) => boolean;
89
+ /**
90
+ * Group events into collapsible sections.
91
+ *
92
+ * Day sections run newest first with undated events in a trailing section;
93
+ * actor and action sections run by descending count, ties broken by label so
94
+ * the order never depends on input order. Events inside every section are
95
+ * newest first.
96
+ */
97
+ export declare const groupAuditEvents: (events: readonly AuditEvent[], groupBy: AuditGroupBy, now?: Date) => AuditGroup[];
98
+ /**
99
+ * Available facet values with counts, derived from the *unfiltered* set so
100
+ * selecting one facet never removes the options for another — a filter UI
101
+ * that can dead-end is worse than no counts.
102
+ */
103
+ export declare const summarizeAuditFacets: (events: readonly AuditEvent[]) => AuditFacetSummary;
104
+ export declare const AUDIT_CSV_COLUMNS: readonly ["id", "timestamp", "actor", "action", "summary", "badge", "correlationId", "evidence"];
105
+ /**
106
+ * Render events as RFC 4180 CSV for the export toolbar. Every field is
107
+ * quoted, embedded quotes are doubled, and a value that would otherwise be
108
+ * read as a spreadsheet formula is prefixed so it stays text.
109
+ */
110
+ export declare const toAuditCsv: (events: readonly AuditEvent[]) => string;
111
+ export interface ActivityDensityCell {
112
+ /** UTC calendar day, `YYYY-MM-DD`. */
113
+ date: string;
114
+ count: number;
115
+ /** 0 for an empty day, otherwise 1–4 scaled against the busiest day in the window. */
116
+ level: number;
117
+ /** Column index, oldest week first. */
118
+ week: number;
119
+ /** Row index, 0 = Sunday. */
120
+ weekday: number;
121
+ }
122
+ export interface ActivityDensity {
123
+ cells: ActivityDensityCell[];
124
+ weeks: number;
125
+ total: number;
126
+ max: number;
127
+ /** First day in the window (the Sunday that opens the oldest column). */
128
+ start: string;
129
+ /** Last day in the window — the reference day. */
130
+ end: string;
131
+ }
132
+ export interface ActivityDensityOptions {
133
+ /** Reference day; the window ends on it. */
134
+ now: Date;
135
+ /** Number of week columns, clamped to 1–53. Defaults to 12. */
136
+ weeks?: number;
137
+ }
138
+ /**
139
+ * Build the calendar heatmap window: whole week columns ending on the
140
+ * reference day's week, rows Sunday → Saturday. Pure and UTC-based, so the
141
+ * same events and reference time always produce the same grid.
142
+ */
143
+ export declare const computeActivityDensity: (events: readonly AuditEvent[], options: ActivityDensityOptions) => ActivityDensity;
@@ -0,0 +1,327 @@
1
+ export const AUDIT_GROUP_BY_VALUES = ["day", "actor", "action"];
2
+ export const AUDIT_GROUP_BY_LABELS = {
3
+ day: "Day",
4
+ actor: "Actor",
5
+ action: "Action",
6
+ };
7
+ export const resolveAuditGroupBy = (value) => value && AUDIT_GROUP_BY_VALUES.includes(value)
8
+ ? value
9
+ : "day";
10
+ export const AUDIT_UNDATED_KEY = "undated";
11
+ export const AUDIT_UNATTRIBUTED_KEY = "";
12
+ const DAY_MS = 24 * 60 * 60 * 1000;
13
+ /**
14
+ * The event's instant in epoch milliseconds, or `null` when it carries no
15
+ * usable timestamp. Undated events are a real case in imported audit data —
16
+ * they sort last rather than being dropped.
17
+ */
18
+ export const auditEventInstant = (event) => {
19
+ if (!event.timestamp) {
20
+ return null;
21
+ }
22
+ const value = new Date(event.timestamp).getTime();
23
+ return Number.isNaN(value) ? null : value;
24
+ };
25
+ /** Newest first; undated events sink to the end without reordering among themselves. */
26
+ const byNewestFirst = (left, right) => {
27
+ const a = auditEventInstant(left);
28
+ const b = auditEventInstant(right);
29
+ if (a === b) {
30
+ return 0;
31
+ }
32
+ if (a === null) {
33
+ return 1;
34
+ }
35
+ if (b === null) {
36
+ return -1;
37
+ }
38
+ return b - a;
39
+ };
40
+ /**
41
+ * The UTC calendar day (`YYYY-MM-DD`) an event belongs to, or `null` when it
42
+ * is undated. Day keys and day labels are both derived in UTC so a viewer's
43
+ * timezone can never split one audit day across two sections. A host that
44
+ * wants local-day semantics offsets the timestamps it supplies.
45
+ */
46
+ export const resolveAuditDay = (event) => {
47
+ const instant = auditEventInstant(event);
48
+ if (instant === null) {
49
+ return null;
50
+ }
51
+ return new Date(instant).toISOString().slice(0, 10);
52
+ };
53
+ const MONTHS = [
54
+ "Jan",
55
+ "Feb",
56
+ "Mar",
57
+ "Apr",
58
+ "May",
59
+ "Jun",
60
+ "Jul",
61
+ "Aug",
62
+ "Sep",
63
+ "Oct",
64
+ "Nov",
65
+ "Dec",
66
+ ];
67
+ /**
68
+ * The one absolute-day formatter — `Aug 13, 2026`, always in UTC. Accepts a
69
+ * `YYYY-MM-DD` key or an instant. Every audit surface formats days through
70
+ * this, so a rendered date can never disagree with the UTC day key that
71
+ * grouped it. An unparseable key is returned unchanged.
72
+ */
73
+ export const formatUtcDay = (value) => {
74
+ const date = typeof value === "string" ? new Date(`${value}T00:00:00.000Z`) : value;
75
+ if (Number.isNaN(date.getTime())) {
76
+ return typeof value === "string" ? value : "";
77
+ }
78
+ return `${MONTHS[date.getUTCMonth()]} ${String(date.getUTCDate())}, ${String(date.getUTCFullYear())}`;
79
+ };
80
+ /** The short UTC month name — `Aug` — for sparse calendar column headers. */
81
+ export const formatUtcMonth = (value) => {
82
+ const date = typeof value === "string" ? new Date(`${value}T00:00:00.000Z`) : value;
83
+ return Number.isNaN(date.getTime()) ? "" : MONTHS[date.getUTCMonth()];
84
+ };
85
+ /**
86
+ * Label a `YYYY-MM-DD` day key, with Today/Yesterday relative labels resolved
87
+ * against the caller's reference time so the output is deterministic rather
88
+ * than clock-dependent.
89
+ */
90
+ export const formatAuditDay = (day, now) => {
91
+ const parsed = new Date(`${day}T00:00:00.000Z`);
92
+ if (Number.isNaN(parsed.getTime())) {
93
+ return day;
94
+ }
95
+ if (now) {
96
+ const today = Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), now.getUTCDate());
97
+ const delta = today - parsed.getTime();
98
+ if (delta === 0) {
99
+ return "Today";
100
+ }
101
+ if (delta === DAY_MS) {
102
+ return "Yesterday";
103
+ }
104
+ }
105
+ return formatUtcDay(parsed);
106
+ };
107
+ const DATE_ONLY = /^\d{4}-\d{2}-\d{2}$/;
108
+ /**
109
+ * Resolve a bound to an instant. A date-only bound (what `<input type="date">`
110
+ * produces) covers the whole UTC day, so `to: "2026-08-13"` includes
111
+ * everything that happened on the 13th rather than only its first
112
+ * millisecond. A full ISO instant is used verbatim.
113
+ */
114
+ const resolveBound = (value, edge) => {
115
+ if (!value) {
116
+ return Number.NaN;
117
+ }
118
+ const resolved = DATE_ONLY.test(value)
119
+ ? `${value}${edge === "start" ? "T00:00:00.000Z" : "T23:59:59.999Z"}`
120
+ : value;
121
+ return new Date(resolved).getTime();
122
+ };
123
+ /**
124
+ * Apply the facet selection. An empty string is "not selected" rather than a
125
+ * match on empty — the same meaning `""` carries as an unset attribute and as
126
+ * a select's all-values option, so the DOM and the engine never disagree.
127
+ *
128
+ * A date bound excludes undated events: an event with no timestamp cannot be
129
+ * shown to fall inside the window, and an audit surface must not imply it does.
130
+ */
131
+ export const filterAuditEvents = (events, facets) => {
132
+ const from = resolveBound(facets.from, "start");
133
+ const to = resolveBound(facets.to, "end");
134
+ const hasFrom = !Number.isNaN(from);
135
+ const hasTo = !Number.isNaN(to);
136
+ return events.filter(event => {
137
+ if (facets.actor && (event.actor?.name ?? "") !== facets.actor) {
138
+ return false;
139
+ }
140
+ if (facets.action && event.action !== facets.action) {
141
+ return false;
142
+ }
143
+ if (facets.correlationId && event.correlationId !== facets.correlationId) {
144
+ return false;
145
+ }
146
+ if (!hasFrom && !hasTo) {
147
+ return true;
148
+ }
149
+ const instant = auditEventInstant(event);
150
+ if (instant === null) {
151
+ return false;
152
+ }
153
+ return (!hasFrom || instant >= from) && (!hasTo || instant <= to);
154
+ });
155
+ };
156
+ /** True when any facet narrows the set — drives the Clear-filters affordance. */
157
+ export const hasAuditFacets = (facets) => Object.values(facets).some(value => value !== undefined && value !== "");
158
+ /**
159
+ * Group events into collapsible sections.
160
+ *
161
+ * Day sections run newest first with undated events in a trailing section;
162
+ * actor and action sections run by descending count, ties broken by label so
163
+ * the order never depends on input order. Events inside every section are
164
+ * newest first.
165
+ */
166
+ export const groupAuditEvents = (events, groupBy, now) => {
167
+ const seeds = new Map();
168
+ const seedFor = (key, label) => {
169
+ let seed = seeds.get(key);
170
+ if (!seed) {
171
+ seed = { key, label, events: [] };
172
+ seeds.set(key, seed);
173
+ }
174
+ return seed;
175
+ };
176
+ for (const event of events) {
177
+ if (groupBy === "day") {
178
+ const day = resolveAuditDay(event);
179
+ seedFor(day ?? AUDIT_UNDATED_KEY, day ? formatAuditDay(day, now) : "Undated").events.push(event);
180
+ }
181
+ else if (groupBy === "actor") {
182
+ const name = event.actor?.name ?? "";
183
+ seedFor(name || AUDIT_UNATTRIBUTED_KEY, name || "Unattributed").events.push(event);
184
+ }
185
+ else {
186
+ seedFor(event.action, event.action).events.push(event);
187
+ }
188
+ }
189
+ const groups = [...seeds.values()].map(seed => {
190
+ const actors = new Set();
191
+ for (const event of seed.events) {
192
+ if (event.actor?.name) {
193
+ actors.add(event.actor.name);
194
+ }
195
+ }
196
+ return {
197
+ key: seed.key,
198
+ label: seed.label,
199
+ events: [...seed.events].sort(byNewestFirst),
200
+ count: seed.events.length,
201
+ actorCount: actors.size,
202
+ };
203
+ });
204
+ if (groupBy === "day") {
205
+ return groups.sort((left, right) => {
206
+ if (left.key === AUDIT_UNDATED_KEY || right.key === AUDIT_UNDATED_KEY) {
207
+ return left.key === right.key ? 0 : left.key === AUDIT_UNDATED_KEY ? 1 : -1;
208
+ }
209
+ return left.key < right.key ? 1 : left.key > right.key ? -1 : 0;
210
+ });
211
+ }
212
+ const trailingKey = groupBy === "actor" ? AUDIT_UNATTRIBUTED_KEY : null;
213
+ return groups.sort((left, right) => {
214
+ if (trailingKey !== null && (left.key === trailingKey || right.key === trailingKey)) {
215
+ return left.key === right.key ? 0 : left.key === trailingKey ? 1 : -1;
216
+ }
217
+ if (left.count !== right.count) {
218
+ return right.count - left.count;
219
+ }
220
+ return left.label < right.label ? -1 : left.label > right.label ? 1 : 0;
221
+ });
222
+ };
223
+ const summarize = (values) => {
224
+ const counts = new Map();
225
+ for (const value of values) {
226
+ counts.set(value, (counts.get(value) ?? 0) + 1);
227
+ }
228
+ return [...counts.entries()]
229
+ .map(([value, count]) => ({ value, label: value || "Unattributed", count }))
230
+ .sort((left, right) => left.count !== right.count
231
+ ? right.count - left.count
232
+ : left.label < right.label
233
+ ? -1
234
+ : left.label > right.label
235
+ ? 1
236
+ : 0);
237
+ };
238
+ /**
239
+ * Available facet values with counts, derived from the *unfiltered* set so
240
+ * selecting one facet never removes the options for another — a filter UI
241
+ * that can dead-end is worse than no counts.
242
+ */
243
+ export const summarizeAuditFacets = (events) => ({
244
+ actors: summarize(events.map(event => event.actor?.name ?? "")),
245
+ actions: summarize(events.map(event => event.action)),
246
+ });
247
+ /** Formulas are data in an audit export, never something a spreadsheet should run. */
248
+ const FORMULA_PREFIXES = ["=", "+", "-", "@", "\t", "\r"];
249
+ const csvField = (value) => {
250
+ const neutralized = FORMULA_PREFIXES.some(prefix => value.startsWith(prefix))
251
+ ? `'${value}`
252
+ : value;
253
+ return `"${neutralized.replaceAll('"', '""')}"`;
254
+ };
255
+ export const AUDIT_CSV_COLUMNS = [
256
+ "id",
257
+ "timestamp",
258
+ "actor",
259
+ "action",
260
+ "summary",
261
+ "badge",
262
+ "correlationId",
263
+ "evidence",
264
+ ];
265
+ /**
266
+ * Render events as RFC 4180 CSV for the export toolbar. Every field is
267
+ * quoted, embedded quotes are doubled, and a value that would otherwise be
268
+ * read as a spreadsheet formula is prefixed so it stays text.
269
+ */
270
+ export const toAuditCsv = (events) => {
271
+ const rows = events.map(event => [
272
+ event.id,
273
+ event.timestamp ?? "",
274
+ event.actor?.name ?? "",
275
+ event.action,
276
+ event.summary ?? "",
277
+ event.badge ?? "",
278
+ event.correlationId ?? "",
279
+ (event.evidence ?? [])
280
+ .map(entry => (entry.href ? `${entry.label} (${entry.href})` : entry.label))
281
+ .join("; "),
282
+ ]
283
+ .map(csvField)
284
+ .join(","));
285
+ return [AUDIT_CSV_COLUMNS.map(csvField).join(","), ...rows].join("\r\n");
286
+ };
287
+ /**
288
+ * Build the calendar heatmap window: whole week columns ending on the
289
+ * reference day's week, rows Sunday → Saturday. Pure and UTC-based, so the
290
+ * same events and reference time always produce the same grid.
291
+ */
292
+ export const computeActivityDensity = (events, options) => {
293
+ const weeks = Math.min(53, Math.max(1, Math.trunc(options.weeks ?? 12)));
294
+ const now = options.now;
295
+ const endDay = Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), now.getUTCDate());
296
+ const endWeekday = new Date(endDay).getUTCDay();
297
+ // Open on the Sunday of the oldest column so every column is a whole week.
298
+ const startDay = endDay - endWeekday * DAY_MS - (weeks - 1) * 7 * DAY_MS;
299
+ const counts = new Map();
300
+ for (const event of events) {
301
+ const day = resolveAuditDay(event);
302
+ if (day) {
303
+ counts.set(day, (counts.get(day) ?? 0) + 1);
304
+ }
305
+ }
306
+ const cells = [];
307
+ let total = 0;
308
+ let max = 0;
309
+ for (let offset = 0; startDay + offset * DAY_MS <= endDay; offset += 1) {
310
+ const date = new Date(startDay + offset * DAY_MS).toISOString().slice(0, 10);
311
+ const count = counts.get(date) ?? 0;
312
+ total += count;
313
+ max = Math.max(max, count);
314
+ cells.push({ date, count, level: 0, week: Math.floor(offset / 7), weekday: offset % 7 });
315
+ }
316
+ for (const cell of cells) {
317
+ cell.level = cell.count === 0 ? 0 : Math.min(4, Math.max(1, Math.ceil((cell.count / max) * 4)));
318
+ }
319
+ return {
320
+ cells,
321
+ weeks,
322
+ total,
323
+ max,
324
+ start: new Date(startDay).toISOString().slice(0, 10),
325
+ end: new Date(endDay).toISOString().slice(0, 10),
326
+ };
327
+ };
@@ -0,0 +1,116 @@
1
+ /**
2
+ * Windowing plan for the grouped audit log.
3
+ *
4
+ * `box-table` can refuse to window expandable rows because a flat table has an
5
+ * alternative: render everything. A ten-thousand-event audit log does not —
6
+ * grouping is the whole point of the surface, and grouped rows are exactly what
7
+ * fixed-height windowing cannot describe. So this is the case that needs the
8
+ * cumulative-offset engine (`src/core/offset-window.ts`) rather than an excuse.
9
+ *
10
+ * The log is windowed as one flat sequence of rows — each group contributes a
11
+ * heading row, then one row per event while it is expanded — because a single
12
+ * day's section can hold thousands of events on its own. Windowing whole groups
13
+ * would leave that case exactly as slow as it is now.
14
+ *
15
+ * The rendered DOM is still nested, so the flat window has to be turned back
16
+ * into sections. That is what `planAuditWindow` does, and the reason it exists
17
+ * as a pure function is the usual one: jsdom has no layout, so a DOM-level test
18
+ * of a windowed surface can only ever observe zeroes.
19
+ *
20
+ * ## The straddling group
21
+ *
22
+ * A group whose heading has scrolled above the viewport still needs its heading
23
+ * rendered: `[part="group-body"]` is `aria-labelledby` its toggle, and a
24
+ * section that arrives without its own heading is both unlabelled and
25
+ * impossible to collapse. So an intersecting group always renders its heading —
26
+ * which means rendering one heading the offset index has already accounted for
27
+ * in `paddingTop`.
28
+ *
29
+ * At most one group can straddle the top edge, so that is a single correction:
30
+ * take the heading's height back out of the top spacer. The events below then
31
+ * land where the scrollbar says they should, and the reclaimed heading sits
32
+ * directly above them.
33
+ */
34
+ import type { OffsetIndex } from "../../core/offset-window.js";
35
+ import type { RowWindow } from "../../core/virtualize.js";
36
+ import type { AuditGroup } from "./types.js";
37
+ /** One row of the flattened log: a section heading, or an event inside one. */
38
+ export interface AuditRow {
39
+ kind: "heading" | "event";
40
+ /** Index into the groups array. */
41
+ groupIndex: number;
42
+ /** Index within that group's events; absent on a heading row. */
43
+ eventIndex?: number;
44
+ }
45
+ export interface AuditRowHeights {
46
+ heading: number;
47
+ event: number;
48
+ }
49
+ /** What to render for one group intersecting the window. */
50
+ export interface PlannedGroup {
51
+ groupIndex: number;
52
+ /** First event of this group to render, inclusive. */
53
+ eventStart: number;
54
+ /** Last event of this group to render, exclusive. */
55
+ eventEnd: number;
56
+ /**
57
+ * True when this group's own heading row is inside the window. False means
58
+ * the heading scrolled above the viewport and is being rendered anyway —
59
+ * `paddingTop` has already been corrected for it.
60
+ */
61
+ headingInWindow: boolean;
62
+ }
63
+ export interface AuditWindowPlan {
64
+ groups: PlannedGroup[];
65
+ paddingTop: number;
66
+ paddingBottom: number;
67
+ totalHeight: number;
68
+ /** The underlying row window, for cheap "did the slice change?" comparisons. */
69
+ window: RowWindow;
70
+ }
71
+ /**
72
+ * The log as a flat row sequence. A collapsed group contributes its heading
73
+ * only — which is exactly why the heights vary and the fixed-height engine
74
+ * cannot describe this surface.
75
+ */
76
+ export declare const flattenAuditRows: (groups: readonly AuditGroup[], collapsedKeys: ReadonlySet<string>) => AuditRow[];
77
+ export declare const auditRowHeights: (rows: readonly AuditRow[], heights: AuditRowHeights) => number[];
78
+ /**
79
+ * The offset index for a row set, so a caller can build it once and reuse it
80
+ * across scroll frames. The index only changes when the rows or the heights do.
81
+ */
82
+ export declare const buildAuditIndex: (rows: readonly AuditRow[], heights: AuditRowHeights) => OffsetIndex;
83
+ /**
84
+ * Which sections to render, and the spacer heights around them.
85
+ *
86
+ * Returns an empty plan for an empty window rather than a plan with no groups
87
+ * and stale padding, so a caller can treat "nothing to render" uniformly.
88
+ */
89
+ export declare const planAuditWindow: (rows: readonly AuditRow[], heights: AuditRowHeights, viewport: {
90
+ viewportHeight: number;
91
+ scrollTop: number;
92
+ overscan?: number;
93
+ /**
94
+ * The scroller's real `scrollHeight`, when the caller can measure it.
95
+ *
96
+ * Two heights per row kind is an estimate, and over thousands of rows the
97
+ * estimate and the rendered reality drift apart — measured at ~0.9% on a
98
+ * 2,000-event log, which sounds harmless and is not: if the estimate runs
99
+ * *long*, scrolling to the real bottom leaves the plan believing there is
100
+ * still a screenful below, and the last rows can never be reached. Content
101
+ * you cannot scroll to is worse than a slightly wrong scrollbar.
102
+ *
103
+ * Given the real height, the scroll position is read as a fraction of the
104
+ * real range and applied to the estimated one, so both ends line up: at the
105
+ * bottom of the scroller the plan is at the bottom of the log.
106
+ */
107
+ contentHeight?: number;
108
+ },
109
+ /**
110
+ * A prebuilt index for these rows and heights, from `buildAuditIndex`.
111
+ *
112
+ * Optional purely for callers that plan once. The scroll path should pass one:
113
+ * building the index is O(n), and rebuilding it per frame would put the O(n)
114
+ * back that the binary search exists to remove.
115
+ */
116
+ prebuilt?: OffsetIndex) => AuditWindowPlan;