@burdenoff/microfe-timecampus 2026.719.3 → 2026.719.4

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.
@@ -6,11 +6,12 @@ import { useGetTimeCampusTimelineQuery as a, useRecategorizeTimeCampusCaptureEve
6
6
  import { PageLayout as s } from "../../components/PageLayout.js";
7
7
  import { EmptyState as c } from "../../components/EmptyState.js";
8
8
  import { useMemo as l, useState as u } from "react";
9
- import { Activity as d, Brain as f, Calendar as p, Coffee as m, Github as h, MessageSquare as g, Users as _, Zap as v } from "lucide-react";
10
- import { jsx as y, jsxs as b } from "react/jsx-runtime";
11
- import { GlassCard as x } from "@burdenoff/fe-libs/ui";
9
+ import { Activity as d, Brain as f, Calendar as p, Coffee as m, Github as h, GripVertical as g, MessageSquare as _, Users as v, Zap as y } from "lucide-react";
10
+ import { jsx as b, jsxs as x } from "react/jsx-runtime";
11
+ import { GlassCard as S } from "@burdenoff/fe-libs/ui";
12
+ import { DndContext as C, DragOverlay as w, KeyboardSensor as T, PointerSensor as E, useDraggable as D, useDroppable as O, useSensor as k, useSensors as A } from "@dnd-kit/core";
12
13
  //#region src/timecampus/pages/timeline/TimelinePage.tsx
13
- var S = {
14
+ var j = {
14
15
  DEEP_WORK: {
15
16
  label: "Deep work",
16
17
  icon: f,
@@ -19,13 +20,13 @@ var S = {
19
20
  },
20
21
  MEETING: {
21
22
  label: "Meeting",
22
- icon: _,
23
+ icon: v,
23
24
  dot: "bg-status-info-bg",
24
25
  chip: "bg-status-info-bg text-status-info-text"
25
26
  },
26
27
  COMMUNICATION: {
27
28
  label: "Communication",
28
- icon: g,
29
+ icon: _,
29
30
  dot: "bg-status-warning-bg",
30
31
  chip: "bg-status-warning-bg text-status-warning-text"
31
32
  },
@@ -41,210 +42,271 @@ var S = {
41
42
  dot: "bg-bg-sunken",
42
43
  chip: "bg-bg-sunken text-text-secondary"
43
44
  }
44
- }, C = {
45
- TIME_ENTRY: v,
46
- MEETING: _,
45
+ }, M = {
46
+ TIME_ENTRY: y,
47
+ MEETING: v,
47
48
  POMODORO: f,
48
- EMAIL: g,
49
+ EMAIL: _,
49
50
  CAPTURE: h
50
- }, w = [
51
+ }, N = [
51
52
  "DEEP_WORK",
52
53
  "MEETING",
53
54
  "COMMUNICATION",
54
55
  "BREAK",
55
56
  "OTHER"
56
57
  ];
57
- function T(e) {
58
+ function P({ category: e, label: n, dotClassName: r, totalSeconds: i }) {
59
+ let { isOver: a, setNodeRef: o } = O({ id: `category:${e}` });
60
+ return /* @__PURE__ */ x("div", {
61
+ ref: o,
62
+ className: `flex items-center gap-2 rounded-lg border px-3 py-2 transition-colors ${a ? "border-action-primary-bg bg-action-primary-bg/10" : "border-border-seam bg-bg-surface"}`,
63
+ children: [
64
+ /* @__PURE__ */ b("span", {
65
+ className: `inline-block h-2.5 w-2.5 rounded-full ${r}`,
66
+ "aria-hidden": !0
67
+ }),
68
+ /* @__PURE__ */ b("span", {
69
+ className: "text-sm font-medium text-text-primary",
70
+ children: n
71
+ }),
72
+ /* @__PURE__ */ b("span", {
73
+ className: "text-sm tabular-nums text-text-secondary",
74
+ children: t(i)
75
+ }),
76
+ a && /* @__PURE__ */ b("span", {
77
+ className: "text-xs font-medium text-primary",
78
+ children: "Drop here"
79
+ })
80
+ ]
81
+ });
82
+ }
83
+ function F({ segment: e, category: r, recategorizing: i, onRecategorize: a }) {
84
+ let o = j[r] ?? j.OTHER, s = M[e.source] ?? d, { attributes: c, listeners: l, setNodeRef: u, isDragging: f } = D({
85
+ id: e.id,
86
+ data: { segment: e },
87
+ disabled: !e.isRecategorizable
88
+ });
89
+ return /* @__PURE__ */ x("li", {
90
+ ref: u,
91
+ className: `flex items-start gap-3 p-4 transition-colors hover:bg-bg-sunken/50 ${f ? "opacity-40" : ""}`,
92
+ children: [
93
+ /* @__PURE__ */ x("div", {
94
+ className: "flex w-16 shrink-0 flex-col items-end pt-0.5 text-xs tabular-nums text-text-secondary",
95
+ children: [/* @__PURE__ */ b("span", { children: n(e.startTime) }), /* @__PURE__ */ b("span", {
96
+ className: "text-text-tertiary",
97
+ children: t(e.durationSeconds)
98
+ })]
99
+ }),
100
+ e.isRecategorizable && /* @__PURE__ */ b("button", {
101
+ type: "button",
102
+ "aria-label": `Drag to recategorize ${e.title || o.label}`,
103
+ className: "mt-1.5 shrink-0 cursor-grab touch-none text-text-tertiary hover:text-text-secondary active:cursor-grabbing",
104
+ ...c,
105
+ ...l,
106
+ children: /* @__PURE__ */ b(g, { className: "h-4 w-4" })
107
+ }),
108
+ /* @__PURE__ */ b("div", {
109
+ className: `mt-0.5 flex h-8 w-8 shrink-0 items-center justify-center rounded-lg ${o.chip}`,
110
+ children: /* @__PURE__ */ b(s, { className: "h-4 w-4" })
111
+ }),
112
+ /* @__PURE__ */ x("div", {
113
+ className: "min-w-0 flex-1",
114
+ children: [/* @__PURE__ */ b("p", {
115
+ className: "truncate text-sm font-medium text-text-primary",
116
+ children: e.title || o.label
117
+ }), /* @__PURE__ */ x("div", {
118
+ className: "mt-1 flex flex-wrap items-center gap-2",
119
+ children: [
120
+ /* @__PURE__ */ b("span", {
121
+ className: `inline-flex rounded-full px-2 py-0.5 text-xs font-medium ${o.chip}`,
122
+ children: o.label
123
+ }),
124
+ e.app && /* @__PURE__ */ b("span", {
125
+ className: "text-xs text-text-secondary",
126
+ children: e.app
127
+ }),
128
+ e.isRecategorizable && /* @__PURE__ */ b("span", {
129
+ className: "text-xs text-text-tertiary",
130
+ children: "via capture"
131
+ })
132
+ ]
133
+ })]
134
+ }),
135
+ e.isRecategorizable && /* @__PURE__ */ b("select", {
136
+ "aria-label": "Recategorize activity",
137
+ disabled: i,
138
+ value: r,
139
+ onChange: (t) => a(e, t.target.value),
140
+ className: "shrink-0 rounded-md border border-border-seam bg-bg-surface px-2 py-1 text-xs text-text-secondary focus:outline-none focus:ring-2 focus:ring-action-primary-bg/40",
141
+ children: N.map((e) => /* @__PURE__ */ b("option", {
142
+ value: e,
143
+ children: j[e].label
144
+ }, e))
145
+ })
146
+ ]
147
+ });
148
+ }
149
+ function I(e) {
58
150
  let t = e.getTimezoneOffset();
59
151
  return (/* @__PURE__ */ new Date(e.getTime() - t * 60 * 1e3)).toISOString().slice(0, 10);
60
152
  }
61
- function E(e) {
153
+ function L(e) {
62
154
  let t = /* @__PURE__ */ new Date(`${e}T00:00:00`), n = /* @__PURE__ */ new Date(`${e}T23:59:59.999`);
63
155
  return {
64
156
  from: t.toISOString(),
65
157
  to: n.toISOString()
66
158
  };
67
159
  }
68
- function D() {
69
- let { workspaceId: m = "" } = e(), [h, g] = u(() => T(/* @__PURE__ */ new Date())), { from: _, to: D } = l(() => E(h), [h]), { data: O, loading: k, error: A, refetch: j } = a({
160
+ function R() {
161
+ let { workspaceId: n = "" } = e(), [m, h] = u(() => I(/* @__PURE__ */ new Date())), [_, v] = u(null), { from: D, to: O } = l(() => L(m), [m]), { data: M, loading: R, error: z, refetch: B } = a({
70
162
  variables: {
71
- from: _,
72
- to: D
163
+ from: D,
164
+ to: O
73
165
  },
74
- skip: !m,
166
+ skip: !n,
75
167
  fetchPolicy: "cache-and-network"
76
- }), [M, { loading: N }] = o(), P = O?.timeCampusTimeline, F = P?.segments ?? [], I = P?.summary, L = l(() => {
168
+ }), [V, { loading: H }] = o(), U = A(k(E, { activationConstraint: { distance: 6 } }), k(T)), W = M?.timeCampusTimeline, G = W?.segments ?? [], K = W?.summary, q = l(() => {
77
169
  let e = /* @__PURE__ */ new Map();
78
- for (let t of I?.byCategory ?? []) e.set(t.category, {
170
+ for (let t of K?.byCategory ?? []) e.set(t.category, {
79
171
  totalSeconds: t.totalSeconds,
80
172
  segmentCount: t.segmentCount
81
173
  });
82
174
  return e;
83
- }, [I]), R = async (e, t) => {
84
- !e.isRecategorizable || e.category === t || (await M({ variables: {
175
+ }, [K]), J = async (e, t) => {
176
+ !e.isRecategorizable || e.category === t || (await V({ variables: {
85
177
  id: e.sourceId,
86
178
  category: t
87
- } }), await j());
88
- }, z = I && I.focusRatio != null ? Math.round(I.focusRatio * 100) : 0;
89
- return /* @__PURE__ */ y(s, {
179
+ } }), await B());
180
+ }, Y = (e) => {
181
+ let t = e.active.data.current?.segment;
182
+ v(t ?? null);
183
+ }, X = (e) => {
184
+ let t = e.active.data.current?.segment;
185
+ v(null);
186
+ let n = e.over?.id;
187
+ if (!t || typeof n != "string") return;
188
+ let r = n.replace(/^category:/, "");
189
+ N.includes(r) && J(t, r);
190
+ }, Z = K && K.focusRatio != null ? Math.round(K.focusRatio * 100) : 0;
191
+ return /* @__PURE__ */ b(s, {
90
192
  title: "Timeline",
91
193
  description: "Your day, assembled automatically",
92
194
  purpose: "See where your time actually went — meetings, deep work, communication and breaks stitched into one honest, chronological view from everything TimeCampus captures, so you can spot the patterns worth changing.",
93
- actions: /* @__PURE__ */ b("label", {
195
+ actions: /* @__PURE__ */ x("label", {
94
196
  className: "flex items-center gap-2 text-sm text-text-secondary",
95
- children: [/* @__PURE__ */ y(p, { className: "h-4 w-4" }), /* @__PURE__ */ y("input", {
197
+ children: [/* @__PURE__ */ b(p, { className: "h-4 w-4" }), /* @__PURE__ */ b("input", {
96
198
  type: "date",
97
- value: h,
98
- max: T(/* @__PURE__ */ new Date()),
99
- onChange: (e) => g(e.target.value),
199
+ value: m,
200
+ max: I(/* @__PURE__ */ new Date()),
201
+ onChange: (e) => h(e.target.value),
100
202
  className: "rounded-md border border-border-seam bg-bg-surface px-3 py-1.5 text-sm text-text-primary focus:outline-none focus:ring-2 focus:ring-action-primary-bg/40"
101
203
  })]
102
204
  }),
103
- children: k && !P ? /* @__PURE__ */ b("div", {
205
+ children: R && !W ? /* @__PURE__ */ x("div", {
104
206
  className: "space-y-6",
105
- children: [/* @__PURE__ */ y(i, {
207
+ children: [/* @__PURE__ */ b(i, {
106
208
  variant: "card",
107
209
  count: 4
108
- }), /* @__PURE__ */ y(i, {
210
+ }), /* @__PURE__ */ b(i, {
109
211
  variant: "list",
110
212
  count: 6
111
213
  })]
112
- }) : A ? /* @__PURE__ */ y("div", {
214
+ }) : z ? /* @__PURE__ */ b("div", {
113
215
  className: "rounded-lg border border-status-error-bg/40 bg-status-error-bg p-4 text-sm text-status-error-text",
114
216
  children: "Failed to load your timeline. Please try again later."
115
- }) : /* @__PURE__ */ b("div", {
116
- className: "space-y-6",
117
- children: [
118
- /* @__PURE__ */ b("div", {
119
- className: "grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-4",
120
- children: [
121
- /* @__PURE__ */ y(r, {
122
- featured: !0,
123
- title: "Tracked time",
124
- value: t(I?.totalTrackedSeconds ?? 0),
125
- icon: /* @__PURE__ */ y(d, { className: "h-5 w-5" }),
126
- description: "Total activity across the day"
127
- }),
128
- /* @__PURE__ */ y(r, {
129
- title: "Focus ratio",
130
- value: `${z}%`,
131
- icon: /* @__PURE__ */ y(f, { className: "h-5 w-5" }),
132
- description: "Share of time in deep work"
133
- }),
134
- /* @__PURE__ */ y(r, {
135
- title: "Context switches",
136
- value: I?.contextSwitches ?? 0,
137
- icon: /* @__PURE__ */ y(v, { className: "h-5 w-5" }),
138
- description: "Category changes through the day"
139
- }),
140
- /* @__PURE__ */ y(r, {
141
- title: "Longest focus block",
142
- value: t(I?.longestFocusBlockSeconds ?? 0),
143
- icon: /* @__PURE__ */ y(f, { className: "h-5 w-5" }),
144
- description: "Your deepest uninterrupted stretch"
145
- })
146
- ]
147
- }),
148
- F.length > 0 && /* @__PURE__ */ b(x, {
149
- treatment: "glass",
150
- className: "p-4 sm:p-6",
151
- children: [/* @__PURE__ */ y("h2", {
152
- className: "mb-4 text-sm font-semibold text-text-primary",
153
- children: "Where the time went"
154
- }), /* @__PURE__ */ y("div", {
155
- className: "flex flex-wrap gap-4",
156
- children: w.filter((e) => L.has(e)).map((e) => {
157
- let n = S[e], r = L.get(e);
158
- return /* @__PURE__ */ b("div", {
159
- className: "flex items-center gap-2 rounded-lg border border-border-seam bg-bg-surface px-3 py-2",
160
- children: [
161
- /* @__PURE__ */ y("span", {
162
- className: `inline-block h-2.5 w-2.5 rounded-full ${n.dot}`,
163
- "aria-hidden": !0
164
- }),
165
- /* @__PURE__ */ y("span", {
166
- className: "text-sm font-medium text-text-primary",
167
- children: n.label
168
- }),
169
- /* @__PURE__ */ y("span", {
170
- className: "text-sm tabular-nums text-text-secondary",
171
- children: t(r.totalSeconds)
172
- })
173
- ]
174
- }, e);
175
- })
176
- })]
177
- }),
178
- F.length === 0 ? /* @__PURE__ */ y(c, {
179
- illustration: "empty-generic",
180
- title: "No activity captured for this day",
181
- description: "As you track time, hold meetings, run focus sessions, and connect shared integrations, your day assembles here automatically — categorized and in order."
182
- }) : /* @__PURE__ */ y(x, {
183
- treatment: "glass",
184
- glow: !0,
185
- className: "overflow-hidden",
186
- children: /* @__PURE__ */ y("ol", {
187
- className: "divide-y divide-border-seam",
188
- children: F.map((e) => {
189
- let r = e.category ?? "OTHER", i = S[r] ?? S.OTHER, a = C[e.source] ?? d;
190
- return /* @__PURE__ */ b("li", {
191
- className: "flex items-start gap-3 p-4 transition-colors hover:bg-bg-sunken/50",
192
- children: [
193
- /* @__PURE__ */ b("div", {
194
- className: "flex w-16 shrink-0 flex-col items-end pt-0.5 text-xs tabular-nums text-text-secondary",
195
- children: [/* @__PURE__ */ y("span", { children: n(e.startTime) }), /* @__PURE__ */ y("span", {
196
- className: "text-text-tertiary",
197
- children: t(e.durationSeconds)
198
- })]
199
- }),
200
- /* @__PURE__ */ y("div", {
201
- className: `mt-0.5 flex h-8 w-8 shrink-0 items-center justify-center rounded-lg ${i.chip}`,
202
- children: /* @__PURE__ */ y(a, { className: "h-4 w-4" })
203
- }),
204
- /* @__PURE__ */ b("div", {
205
- className: "min-w-0 flex-1",
206
- children: [/* @__PURE__ */ y("p", {
207
- className: "truncate text-sm font-medium text-text-primary",
208
- children: e.title || i.label
209
- }), /* @__PURE__ */ b("div", {
210
- className: "mt-1 flex flex-wrap items-center gap-2",
211
- children: [
212
- /* @__PURE__ */ y("span", {
213
- className: `inline-flex rounded-full px-2 py-0.5 text-xs font-medium ${i.chip}`,
214
- children: i.label
215
- }),
216
- e.app && /* @__PURE__ */ y("span", {
217
- className: "text-xs text-text-secondary",
218
- children: e.app
219
- }),
220
- e.isRecategorizable && /* @__PURE__ */ y("span", {
221
- className: "text-xs text-text-tertiary",
222
- children: "via capture"
223
- })
224
- ]
225
- })]
226
- }),
227
- e.isRecategorizable && /* @__PURE__ */ y("select", {
228
- "aria-label": "Recategorize activity",
229
- disabled: N,
230
- value: r,
231
- onChange: (t) => R(e, t.target.value),
232
- className: "shrink-0 rounded-md border border-border-seam bg-bg-surface px-2 py-1 text-xs text-text-secondary focus:outline-none focus:ring-2 focus:ring-action-primary-bg/40",
233
- children: w.map((e) => /* @__PURE__ */ y("option", {
234
- value: e,
235
- children: S[e].label
236
- }, e))
237
- })
238
- ]
239
- }, e.id);
217
+ }) : /* @__PURE__ */ x(C, {
218
+ sensors: U,
219
+ onDragStart: Y,
220
+ onDragEnd: X,
221
+ children: [/* @__PURE__ */ x("div", {
222
+ className: "space-y-6",
223
+ children: [
224
+ /* @__PURE__ */ x("div", {
225
+ className: "grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-4",
226
+ children: [
227
+ /* @__PURE__ */ b(r, {
228
+ featured: !0,
229
+ title: "Tracked time",
230
+ value: t(K?.totalTrackedSeconds ?? 0),
231
+ icon: /* @__PURE__ */ b(d, { className: "h-5 w-5" }),
232
+ description: "Total activity across the day"
233
+ }),
234
+ /* @__PURE__ */ b(r, {
235
+ title: "Focus ratio",
236
+ value: `${Z}%`,
237
+ icon: /* @__PURE__ */ b(f, { className: "h-5 w-5" }),
238
+ description: "Share of time in deep work"
239
+ }),
240
+ /* @__PURE__ */ b(r, {
241
+ title: "Context switches",
242
+ value: K?.contextSwitches ?? 0,
243
+ icon: /* @__PURE__ */ b(y, { className: "h-5 w-5" }),
244
+ description: "Category changes through the day"
245
+ }),
246
+ /* @__PURE__ */ b(r, {
247
+ title: "Longest focus block",
248
+ value: t(K?.longestFocusBlockSeconds ?? 0),
249
+ icon: /* @__PURE__ */ b(f, { className: "h-5 w-5" }),
250
+ description: "Your deepest uninterrupted stretch"
251
+ })
252
+ ]
253
+ }),
254
+ G.length > 0 && /* @__PURE__ */ x(S, {
255
+ treatment: "glass",
256
+ className: "p-4 sm:p-6",
257
+ children: [
258
+ /* @__PURE__ */ b("h2", {
259
+ className: "mb-1 text-sm font-semibold text-text-primary",
260
+ children: "Where the time went"
261
+ }),
262
+ /* @__PURE__ */ b("p", {
263
+ className: "mb-4 text-xs text-text-secondary",
264
+ children: "Drag a segment from the timeline below onto a category to recategorize it — or use its dropdown."
265
+ }),
266
+ /* @__PURE__ */ b("div", {
267
+ className: "flex flex-wrap gap-4",
268
+ children: N.filter((e) => q.has(e)).map((e) => {
269
+ let t = j[e], n = q.get(e);
270
+ return /* @__PURE__ */ b(P, {
271
+ category: e,
272
+ label: t.label,
273
+ dotClassName: t.dot,
274
+ totalSeconds: n.totalSeconds
275
+ }, e);
276
+ })
277
+ })
278
+ ]
279
+ }),
280
+ G.length === 0 ? /* @__PURE__ */ b(c, {
281
+ illustration: "empty-generic",
282
+ title: "No activity captured for this day",
283
+ description: "As you track time, hold meetings, run focus sessions, and connect shared integrations, your day assembles here automatically — categorized and in order."
284
+ }) : /* @__PURE__ */ b(S, {
285
+ treatment: "glass",
286
+ glow: !0,
287
+ className: "overflow-hidden",
288
+ children: /* @__PURE__ */ b("ol", {
289
+ className: "divide-y divide-border-seam",
290
+ children: G.map((e) => /* @__PURE__ */ b(F, {
291
+ segment: e,
292
+ category: e.category ?? "OTHER",
293
+ recategorizing: H,
294
+ onRecategorize: J
295
+ }, e.id))
240
296
  })
241
297
  })
242
- })
243
- ]
298
+ ]
299
+ }), /* @__PURE__ */ b(w, { children: _ && /* @__PURE__ */ x("div", {
300
+ className: "flex items-center gap-2 rounded-lg border border-border-seam bg-bg-surface px-3 py-2 shadow-pop",
301
+ children: [/* @__PURE__ */ b(g, { className: "h-4 w-4 text-text-tertiary" }), /* @__PURE__ */ b("span", {
302
+ className: "text-sm font-medium text-text-primary",
303
+ children: _.title || j[_.category ?? "OTHER"].label
304
+ })]
305
+ }) })]
244
306
  })
245
307
  });
246
308
  }
247
309
  //#endregion
248
- export { D as TimelinePage };
310
+ export { R as TimelinePage };
249
311
 
250
312
  //# sourceMappingURL=TimelinePage.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"TimelinePage.js","names":[],"sources":["../../../../src/timecampus/pages/timeline/TimelinePage.tsx"],"sourcesContent":["import { useMemo, useState } from \"react\";\nimport {\n Activity,\n Brain,\n Calendar,\n Coffee,\n Github,\n MessageSquare,\n Users,\n Zap,\n} from \"lucide-react\";\nimport { GlassCard } from \"@burdenoff/fe-libs/ui\";\nimport { PageLayout } from \"../../components/PageLayout\";\nimport { StatCard } from \"../../components/StatCard\";\nimport { EmptyState } from \"../../components/EmptyState\";\nimport { LoadingSkeleton } from \"../../components/LoadingSkeleton\";\nimport { formatDuration, formatTime } from \"../../utils/helpers\";\nimport { useTimeCampus } from \"../../providers/TimeCampusProvider\";\nimport {\n useGetTimeCampusTimelineQuery,\n useRecategorizeTimeCampusCaptureEventMutation,\n type TimelineSegmentFieldsFragment,\n} from \"@/generated/wspace-operations\";\n\ntype Category = \"MEETING\" | \"DEEP_WORK\" | \"COMMUNICATION\" | \"BREAK\" | \"OTHER\";\n\nconst CATEGORY_META: Record<\n Category,\n { label: string; icon: typeof Brain; dot: string; chip: string }\n> = {\n DEEP_WORK: {\n label: \"Deep work\",\n icon: Brain,\n dot: \"bg-status-success-bg\",\n chip: \"bg-status-success-bg text-status-success-text\",\n },\n MEETING: {\n label: \"Meeting\",\n icon: Users,\n dot: \"bg-status-info-bg\",\n chip: \"bg-status-info-bg text-status-info-text\",\n },\n COMMUNICATION: {\n label: \"Communication\",\n icon: MessageSquare,\n dot: \"bg-status-warning-bg\",\n chip: \"bg-status-warning-bg text-status-warning-text\",\n },\n BREAK: {\n label: \"Break\",\n icon: Coffee,\n dot: \"bg-bg-sunken\",\n chip: \"bg-bg-sunken text-text-secondary\",\n },\n OTHER: {\n label: \"Other\",\n icon: Activity,\n dot: \"bg-bg-sunken\",\n chip: \"bg-bg-sunken text-text-secondary\",\n },\n};\n\nconst SOURCE_ICON: Record<string, typeof Brain> = {\n TIME_ENTRY: Zap,\n MEETING: Users,\n POMODORO: Brain,\n EMAIL: MessageSquare,\n CAPTURE: Github,\n};\n\nconst CATEGORY_ORDER: Category[] = [\n \"DEEP_WORK\",\n \"MEETING\",\n \"COMMUNICATION\",\n \"BREAK\",\n \"OTHER\",\n];\n\nfunction toLocalDateInput(date: Date): string {\n const offset = date.getTimezoneOffset();\n const local = new Date(date.getTime() - offset * 60 * 1000);\n return local.toISOString().slice(0, 10);\n}\n\nfunction dayBounds(dateStr: string): { from: string; to: string } {\n const start = new Date(`${dateStr}T00:00:00`);\n const end = new Date(`${dateStr}T23:59:59.999`);\n return { from: start.toISOString(), to: end.toISOString() };\n}\n\nexport function TimelinePage() {\n const { workspaceId = \"\" } = useTimeCampus();\n const [dateStr, setDateStr] = useState(() => toLocalDateInput(new Date()));\n\n const { from, to } = useMemo(() => dayBounds(dateStr), [dateStr]);\n\n const { data, loading, error, refetch } = useGetTimeCampusTimelineQuery({\n variables: { from, to },\n skip: !workspaceId,\n fetchPolicy: \"cache-and-network\",\n });\n\n const [recategorize, { loading: recategorizing }] =\n useRecategorizeTimeCampusCaptureEventMutation();\n\n const timeline = data?.timeCampusTimeline;\n const segments = timeline?.segments ?? [];\n const summary = timeline?.summary;\n\n const totalsByCategory = useMemo(() => {\n const map = new Map<\n string,\n { totalSeconds: number; segmentCount: number }\n >();\n for (const t of summary?.byCategory ?? []) {\n map.set(t.category, {\n totalSeconds: t.totalSeconds,\n segmentCount: t.segmentCount,\n });\n }\n return map;\n }, [summary]);\n\n const handleRecategorize = async (\n segment: TimelineSegmentFieldsFragment,\n category: Category,\n ) => {\n if (!segment.isRecategorizable || segment.category === category) return;\n await recategorize({\n variables: { id: segment.sourceId, category },\n });\n await refetch();\n };\n\n const focusPct =\n summary && summary.focusRatio != null\n ? Math.round(summary.focusRatio * 100)\n : 0;\n\n return (\n <PageLayout\n title=\"Timeline\"\n description=\"Your day, assembled automatically\"\n purpose=\"See where your time actually went — meetings, deep work, communication and breaks stitched into one honest, chronological view from everything TimeCampus captures, so you can spot the patterns worth changing.\"\n actions={\n <label className=\"flex items-center gap-2 text-sm text-text-secondary\">\n <Calendar className=\"h-4 w-4\" />\n <input\n type=\"date\"\n value={dateStr}\n max={toLocalDateInput(new Date())}\n onChange={(e) => setDateStr(e.target.value)}\n className=\"rounded-md border border-border-seam bg-bg-surface px-3 py-1.5 text-sm text-text-primary focus:outline-none focus:ring-2 focus:ring-action-primary-bg/40\"\n />\n </label>\n }\n >\n {loading && !timeline ? (\n <div className=\"space-y-6\">\n <LoadingSkeleton variant=\"card\" count={4} />\n <LoadingSkeleton variant=\"list\" count={6} />\n </div>\n ) : error ? (\n <div className=\"rounded-lg border border-status-error-bg/40 bg-status-error-bg p-4 text-sm text-status-error-text\">\n Failed to load your timeline. Please try again later.\n </div>\n ) : (\n <div className=\"space-y-6\">\n {/* Summary */}\n <div className=\"grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-4\">\n <StatCard\n featured\n title=\"Tracked time\"\n value={formatDuration(summary?.totalTrackedSeconds ?? 0)}\n icon={<Activity className=\"h-5 w-5\" />}\n description=\"Total activity across the day\"\n />\n <StatCard\n title=\"Focus ratio\"\n value={`${focusPct}%`}\n icon={<Brain className=\"h-5 w-5\" />}\n description=\"Share of time in deep work\"\n />\n <StatCard\n title=\"Context switches\"\n value={summary?.contextSwitches ?? 0}\n icon={<Zap className=\"h-5 w-5\" />}\n description=\"Category changes through the day\"\n />\n <StatCard\n title=\"Longest focus block\"\n value={formatDuration(summary?.longestFocusBlockSeconds ?? 0)}\n icon={<Brain className=\"h-5 w-5\" />}\n description=\"Your deepest uninterrupted stretch\"\n />\n </div>\n\n {/* Category breakdown */}\n {segments.length > 0 && (\n <GlassCard treatment=\"glass\" className=\"p-4 sm:p-6\">\n <h2 className=\"mb-4 text-sm font-semibold text-text-primary\">\n Where the time went\n </h2>\n <div className=\"flex flex-wrap gap-4\">\n {CATEGORY_ORDER.filter((c) => totalsByCategory.has(c)).map(\n (category) => {\n const meta = CATEGORY_META[category];\n const total = totalsByCategory.get(category)!;\n return (\n <div\n key={category}\n className=\"flex items-center gap-2 rounded-lg border border-border-seam bg-bg-surface px-3 py-2\"\n >\n <span\n className={`inline-block h-2.5 w-2.5 rounded-full ${meta.dot}`}\n aria-hidden\n />\n <span className=\"text-sm font-medium text-text-primary\">\n {meta.label}\n </span>\n <span className=\"text-sm tabular-nums text-text-secondary\">\n {formatDuration(total.totalSeconds)}\n </span>\n </div>\n );\n },\n )}\n </div>\n </GlassCard>\n )}\n\n {/* Chronological timeline */}\n {segments.length === 0 ? (\n <EmptyState\n illustration=\"empty-generic\"\n title=\"No activity captured for this day\"\n description=\"As you track time, hold meetings, run focus sessions, and connect shared integrations, your day assembles here automatically — categorized and in order.\"\n />\n ) : (\n <GlassCard treatment=\"glass\" glow className=\"overflow-hidden\">\n <ol className=\"divide-y divide-border-seam\">\n {segments.map((segment: TimelineSegmentFieldsFragment) => {\n const category = (segment.category as Category) ?? \"OTHER\";\n const meta = CATEGORY_META[category] ?? CATEGORY_META.OTHER;\n const SourceIcon = SOURCE_ICON[segment.source] ?? Activity;\n return (\n <li\n key={segment.id}\n className=\"flex items-start gap-3 p-4 transition-colors hover:bg-bg-sunken/50\"\n >\n <div className=\"flex w-16 shrink-0 flex-col items-end pt-0.5 text-xs tabular-nums text-text-secondary\">\n <span>{formatTime(segment.startTime)}</span>\n <span className=\"text-text-tertiary\">\n {formatDuration(segment.durationSeconds)}\n </span>\n </div>\n <div\n className={`mt-0.5 flex h-8 w-8 shrink-0 items-center justify-center rounded-lg ${meta.chip}`}\n >\n <SourceIcon className=\"h-4 w-4\" />\n </div>\n <div className=\"min-w-0 flex-1\">\n <p className=\"truncate text-sm font-medium text-text-primary\">\n {segment.title || meta.label}\n </p>\n <div className=\"mt-1 flex flex-wrap items-center gap-2\">\n <span\n className={`inline-flex rounded-full px-2 py-0.5 text-xs font-medium ${meta.chip}`}\n >\n {meta.label}\n </span>\n {segment.app && (\n <span className=\"text-xs text-text-secondary\">\n {segment.app}\n </span>\n )}\n {segment.isRecategorizable && (\n <span className=\"text-xs text-text-tertiary\">\n via capture\n </span>\n )}\n </div>\n </div>\n {segment.isRecategorizable && (\n <select\n aria-label=\"Recategorize activity\"\n disabled={recategorizing}\n value={category}\n onChange={(e) =>\n handleRecategorize(\n segment,\n e.target.value as Category,\n )\n }\n className=\"shrink-0 rounded-md border border-border-seam bg-bg-surface px-2 py-1 text-xs text-text-secondary focus:outline-none focus:ring-2 focus:ring-action-primary-bg/40\"\n >\n {CATEGORY_ORDER.map((c) => (\n <option key={c} value={c}>\n {CATEGORY_META[c].label}\n </option>\n ))}\n </select>\n )}\n </li>\n );\n })}\n </ol>\n </GlassCard>\n )}\n </div>\n )}\n </PageLayout>\n );\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"TimelinePage.js","names":[],"sources":["../../../../src/timecampus/pages/timeline/TimelinePage.tsx"],"sourcesContent":["import { useMemo, useState } from \"react\";\nimport {\n DndContext,\n DragOverlay,\n KeyboardSensor,\n PointerSensor,\n useDraggable,\n useDroppable,\n useSensor,\n useSensors,\n type DragEndEvent,\n type DragStartEvent,\n} from \"@dnd-kit/core\";\nimport {\n Activity,\n Brain,\n Calendar,\n Coffee,\n Github,\n GripVertical,\n MessageSquare,\n Users,\n Zap,\n} from \"lucide-react\";\nimport { GlassCard } from \"@burdenoff/fe-libs/ui\";\nimport { PageLayout } from \"../../components/PageLayout\";\nimport { StatCard } from \"../../components/StatCard\";\nimport { EmptyState } from \"../../components/EmptyState\";\nimport { LoadingSkeleton } from \"../../components/LoadingSkeleton\";\nimport { formatDuration, formatTime } from \"../../utils/helpers\";\nimport { useTimeCampus } from \"../../providers/TimeCampusProvider\";\nimport {\n useGetTimeCampusTimelineQuery,\n useRecategorizeTimeCampusCaptureEventMutation,\n type TimelineSegmentFieldsFragment,\n} from \"@/generated/wspace-operations\";\n\ntype Category = \"MEETING\" | \"DEEP_WORK\" | \"COMMUNICATION\" | \"BREAK\" | \"OTHER\";\n\nconst CATEGORY_META: Record<\n Category,\n { label: string; icon: typeof Brain; dot: string; chip: string }\n> = {\n DEEP_WORK: {\n label: \"Deep work\",\n icon: Brain,\n dot: \"bg-status-success-bg\",\n chip: \"bg-status-success-bg text-status-success-text\",\n },\n MEETING: {\n label: \"Meeting\",\n icon: Users,\n dot: \"bg-status-info-bg\",\n chip: \"bg-status-info-bg text-status-info-text\",\n },\n COMMUNICATION: {\n label: \"Communication\",\n icon: MessageSquare,\n dot: \"bg-status-warning-bg\",\n chip: \"bg-status-warning-bg text-status-warning-text\",\n },\n BREAK: {\n label: \"Break\",\n icon: Coffee,\n dot: \"bg-bg-sunken\",\n chip: \"bg-bg-sunken text-text-secondary\",\n },\n OTHER: {\n label: \"Other\",\n icon: Activity,\n dot: \"bg-bg-sunken\",\n chip: \"bg-bg-sunken text-text-secondary\",\n },\n};\n\nconst SOURCE_ICON: Record<string, typeof Brain> = {\n TIME_ENTRY: Zap,\n MEETING: Users,\n POMODORO: Brain,\n EMAIL: MessageSquare,\n CAPTURE: Github,\n};\n\nconst CATEGORY_ORDER: Category[] = [\n \"DEEP_WORK\",\n \"MEETING\",\n \"COMMUNICATION\",\n \"BREAK\",\n \"OTHER\",\n];\n\n/** Droppable target: a category chip in the \"Where the time went\" summary. */\nfunction DroppableCategoryChip({\n category,\n label,\n dotClassName,\n totalSeconds,\n}: {\n category: Category;\n label: string;\n dotClassName: string;\n totalSeconds: number;\n}) {\n const { isOver, setNodeRef } = useDroppable({ id: `category:${category}` });\n return (\n <div\n ref={setNodeRef}\n className={`flex items-center gap-2 rounded-lg border px-3 py-2 transition-colors ${\n isOver\n ? \"border-action-primary-bg bg-action-primary-bg/10\"\n : \"border-border-seam bg-bg-surface\"\n }`}\n >\n <span\n className={`inline-block h-2.5 w-2.5 rounded-full ${dotClassName}`}\n aria-hidden\n />\n <span className=\"text-sm font-medium text-text-primary\">{label}</span>\n <span className=\"text-sm tabular-nums text-text-secondary\">\n {formatDuration(totalSeconds)}\n </span>\n {isOver && (\n <span className=\"text-xs font-medium text-primary\">Drop here</span>\n )}\n </div>\n );\n}\n\n/** Draggable row for a single timeline segment, with the existing dropdown kept as an accessible fallback. */\nfunction DraggableSegmentRow({\n segment,\n category,\n recategorizing,\n onRecategorize,\n}: {\n segment: TimelineSegmentFieldsFragment;\n category: Category;\n recategorizing: boolean;\n onRecategorize: (\n segment: TimelineSegmentFieldsFragment,\n category: Category,\n ) => void;\n}) {\n const meta = CATEGORY_META[category] ?? CATEGORY_META.OTHER;\n const SourceIcon = SOURCE_ICON[segment.source] ?? Activity;\n const { attributes, listeners, setNodeRef, isDragging } = useDraggable({\n id: segment.id,\n data: { segment },\n disabled: !segment.isRecategorizable,\n });\n\n return (\n <li\n ref={setNodeRef}\n className={`flex items-start gap-3 p-4 transition-colors hover:bg-bg-sunken/50 ${\n isDragging ? \"opacity-40\" : \"\"\n }`}\n >\n <div className=\"flex w-16 shrink-0 flex-col items-end pt-0.5 text-xs tabular-nums text-text-secondary\">\n <span>{formatTime(segment.startTime)}</span>\n <span className=\"text-text-tertiary\">\n {formatDuration(segment.durationSeconds)}\n </span>\n </div>\n {segment.isRecategorizable && (\n <button\n type=\"button\"\n aria-label={`Drag to recategorize ${segment.title || meta.label}`}\n className=\"mt-1.5 shrink-0 cursor-grab touch-none text-text-tertiary hover:text-text-secondary active:cursor-grabbing\"\n {...attributes}\n {...listeners}\n >\n <GripVertical className=\"h-4 w-4\" />\n </button>\n )}\n <div\n className={`mt-0.5 flex h-8 w-8 shrink-0 items-center justify-center rounded-lg ${meta.chip}`}\n >\n <SourceIcon className=\"h-4 w-4\" />\n </div>\n <div className=\"min-w-0 flex-1\">\n <p className=\"truncate text-sm font-medium text-text-primary\">\n {segment.title || meta.label}\n </p>\n <div className=\"mt-1 flex flex-wrap items-center gap-2\">\n <span\n className={`inline-flex rounded-full px-2 py-0.5 text-xs font-medium ${meta.chip}`}\n >\n {meta.label}\n </span>\n {segment.app && (\n <span className=\"text-xs text-text-secondary\">{segment.app}</span>\n )}\n {segment.isRecategorizable && (\n <span className=\"text-xs text-text-tertiary\">via capture</span>\n )}\n </div>\n </div>\n {segment.isRecategorizable && (\n <select\n aria-label=\"Recategorize activity\"\n disabled={recategorizing}\n value={category}\n onChange={(e) => onRecategorize(segment, e.target.value as Category)}\n className=\"shrink-0 rounded-md border border-border-seam bg-bg-surface px-2 py-1 text-xs text-text-secondary focus:outline-none focus:ring-2 focus:ring-action-primary-bg/40\"\n >\n {CATEGORY_ORDER.map((c) => (\n <option key={c} value={c}>\n {CATEGORY_META[c].label}\n </option>\n ))}\n </select>\n )}\n </li>\n );\n}\n\nfunction toLocalDateInput(date: Date): string {\n const offset = date.getTimezoneOffset();\n const local = new Date(date.getTime() - offset * 60 * 1000);\n return local.toISOString().slice(0, 10);\n}\n\nfunction dayBounds(dateStr: string): { from: string; to: string } {\n const start = new Date(`${dateStr}T00:00:00`);\n const end = new Date(`${dateStr}T23:59:59.999`);\n return { from: start.toISOString(), to: end.toISOString() };\n}\n\nexport function TimelinePage() {\n const { workspaceId = \"\" } = useTimeCampus();\n const [dateStr, setDateStr] = useState(() => toLocalDateInput(new Date()));\n const [draggingSegment, setDraggingSegment] =\n useState<TimelineSegmentFieldsFragment | null>(null);\n\n const { from, to } = useMemo(() => dayBounds(dateStr), [dateStr]);\n\n const { data, loading, error, refetch } = useGetTimeCampusTimelineQuery({\n variables: { from, to },\n skip: !workspaceId,\n fetchPolicy: \"cache-and-network\",\n });\n\n const [recategorize, { loading: recategorizing }] =\n useRecategorizeTimeCampusCaptureEventMutation();\n\n const sensors = useSensors(\n useSensor(PointerSensor, { activationConstraint: { distance: 6 } }),\n useSensor(KeyboardSensor),\n );\n\n const timeline = data?.timeCampusTimeline;\n const segments = timeline?.segments ?? [];\n const summary = timeline?.summary;\n\n const totalsByCategory = useMemo(() => {\n const map = new Map<\n string,\n { totalSeconds: number; segmentCount: number }\n >();\n for (const t of summary?.byCategory ?? []) {\n map.set(t.category, {\n totalSeconds: t.totalSeconds,\n segmentCount: t.segmentCount,\n });\n }\n return map;\n }, [summary]);\n\n const handleRecategorize = async (\n segment: TimelineSegmentFieldsFragment,\n category: Category,\n ) => {\n if (!segment.isRecategorizable || segment.category === category) return;\n await recategorize({\n variables: { id: segment.sourceId, category },\n });\n await refetch();\n };\n\n const handleDragStart = (event: DragStartEvent) => {\n const segment = event.active.data.current?.segment as\n TimelineSegmentFieldsFragment | undefined;\n setDraggingSegment(segment ?? null);\n };\n\n const handleDragEnd = (event: DragEndEvent) => {\n const segment = event.active.data.current?.segment as\n TimelineSegmentFieldsFragment | undefined;\n setDraggingSegment(null);\n const overId = event.over?.id;\n if (!segment || typeof overId !== \"string\") return;\n const category = overId.replace(/^category:/, \"\") as Category;\n if (!CATEGORY_ORDER.includes(category)) return;\n void handleRecategorize(segment, category);\n };\n\n const focusPct =\n summary && summary.focusRatio != null\n ? Math.round(summary.focusRatio * 100)\n : 0;\n\n return (\n <PageLayout\n title=\"Timeline\"\n description=\"Your day, assembled automatically\"\n purpose=\"See where your time actually went — meetings, deep work, communication and breaks stitched into one honest, chronological view from everything TimeCampus captures, so you can spot the patterns worth changing.\"\n actions={\n <label className=\"flex items-center gap-2 text-sm text-text-secondary\">\n <Calendar className=\"h-4 w-4\" />\n <input\n type=\"date\"\n value={dateStr}\n max={toLocalDateInput(new Date())}\n onChange={(e) => setDateStr(e.target.value)}\n className=\"rounded-md border border-border-seam bg-bg-surface px-3 py-1.5 text-sm text-text-primary focus:outline-none focus:ring-2 focus:ring-action-primary-bg/40\"\n />\n </label>\n }\n >\n {loading && !timeline ? (\n <div className=\"space-y-6\">\n <LoadingSkeleton variant=\"card\" count={4} />\n <LoadingSkeleton variant=\"list\" count={6} />\n </div>\n ) : error ? (\n <div className=\"rounded-lg border border-status-error-bg/40 bg-status-error-bg p-4 text-sm text-status-error-text\">\n Failed to load your timeline. Please try again later.\n </div>\n ) : (\n <DndContext\n sensors={sensors}\n onDragStart={handleDragStart}\n onDragEnd={handleDragEnd}\n >\n <div className=\"space-y-6\">\n {/* Summary */}\n <div className=\"grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-4\">\n <StatCard\n featured\n title=\"Tracked time\"\n value={formatDuration(summary?.totalTrackedSeconds ?? 0)}\n icon={<Activity className=\"h-5 w-5\" />}\n description=\"Total activity across the day\"\n />\n <StatCard\n title=\"Focus ratio\"\n value={`${focusPct}%`}\n icon={<Brain className=\"h-5 w-5\" />}\n description=\"Share of time in deep work\"\n />\n <StatCard\n title=\"Context switches\"\n value={summary?.contextSwitches ?? 0}\n icon={<Zap className=\"h-5 w-5\" />}\n description=\"Category changes through the day\"\n />\n <StatCard\n title=\"Longest focus block\"\n value={formatDuration(summary?.longestFocusBlockSeconds ?? 0)}\n icon={<Brain className=\"h-5 w-5\" />}\n description=\"Your deepest uninterrupted stretch\"\n />\n </div>\n\n {/* Category breakdown — also the drop targets for drag-to-recategorize */}\n {segments.length > 0 && (\n <GlassCard treatment=\"glass\" className=\"p-4 sm:p-6\">\n <h2 className=\"mb-1 text-sm font-semibold text-text-primary\">\n Where the time went\n </h2>\n <p className=\"mb-4 text-xs text-text-secondary\">\n Drag a segment from the timeline below onto a category to\n recategorize it — or use its dropdown.\n </p>\n <div className=\"flex flex-wrap gap-4\">\n {CATEGORY_ORDER.filter((c) => totalsByCategory.has(c)).map(\n (category) => {\n const meta = CATEGORY_META[category];\n const total = totalsByCategory.get(category)!;\n return (\n <DroppableCategoryChip\n key={category}\n category={category}\n label={meta.label}\n dotClassName={meta.dot}\n totalSeconds={total.totalSeconds}\n />\n );\n },\n )}\n </div>\n </GlassCard>\n )}\n\n {/* Chronological timeline */}\n {segments.length === 0 ? (\n <EmptyState\n illustration=\"empty-generic\"\n title=\"No activity captured for this day\"\n description=\"As you track time, hold meetings, run focus sessions, and connect shared integrations, your day assembles here automatically — categorized and in order.\"\n />\n ) : (\n <GlassCard treatment=\"glass\" glow className=\"overflow-hidden\">\n <ol className=\"divide-y divide-border-seam\">\n {segments.map((segment: TimelineSegmentFieldsFragment) => (\n <DraggableSegmentRow\n key={segment.id}\n segment={segment}\n category={(segment.category as Category) ?? \"OTHER\"}\n recategorizing={recategorizing}\n onRecategorize={handleRecategorize}\n />\n ))}\n </ol>\n </GlassCard>\n )}\n </div>\n\n <DragOverlay>\n {draggingSegment && (\n <div className=\"flex items-center gap-2 rounded-lg border border-border-seam bg-bg-surface px-3 py-2 shadow-pop\">\n <GripVertical className=\"h-4 w-4 text-text-tertiary\" />\n <span className=\"text-sm font-medium text-text-primary\">\n {draggingSegment.title ||\n CATEGORY_META[\n (draggingSegment.category as Category) ?? \"OTHER\"\n ].label}\n </span>\n </div>\n )}\n </DragOverlay>\n </DndContext>\n )}\n </PageLayout>\n );\n}\n"],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@burdenoff/microfe-timecampus",
3
- "version": "2026.719.3",
3
+ "version": "2026.719.4",
4
4
  "description": "Burdenoff TimeCampus Microfrontend - AI-powered time management and productivity platform",
5
5
  "type": "module",
6
6
  "files": [
@@ -51,6 +51,7 @@
51
51
  "dependencies": {
52
52
  "@apollo/client": "^4.0.11",
53
53
  "@burdenoff/fe-libs": "2026.713.6",
54
+ "@dnd-kit/core": "^6.3.1",
54
55
  "clsx": "^2.1.1",
55
56
  "date-fns": "^4.1.0",
56
57
  "graphql": "^16.12.0",